
Ethan Collins
Pattern Recognition Specialist

capsolver-agent as the tool-adapter layer over capsolver-core, with LangChain tools for agent frameworks and browser methods for Playwright sessions.A LangGraph Cloudflare Turnstile integration lets an agent recover from a verification step inside an authorized browser workflow and then resume the original task. The graph should not ask the language model to click or reason through the widget. Instead, the model or browser controller detects that the workflow is blocked, the graph evaluates policy, and a deterministic adapter calls the documented CapSolver capability.
CapSolver documents this division of labor in its agent tools guide: the model handles navigation and decisions, capsolver-agent exposes tool schemas and an executor, and capsolver-core performs detection, solving, and browser fill-back.
This architecture gives LangGraph a useful role. It can make recovery observable, enforce retry budgets, route sensitive actions to a human, and ensure the browser verifies success before the graph continues.
Use an isolated Python environment. CapSolver's current official guide installs the core and agent packages from GitHub:
python -m venv .venv
source .venv/bin/activate
pip install git+https://github.com/capsolver-ai/capsolver-core.git
pip install "capsolver-agent[langchain] @ git+https://github.com/capsolver-ai/capsolver-agent.git"
pip install langchain-openai langgraph playwright
playwright install chromium
Place CAPSOLVER_API_KEY and any model credential in an approved secret store. Do not write real values into graph state, checkpoints, prompts, tracing events, or source files.
You also need:
Keep only non-secret operational data in the graph state:
from typing import Literal, TypedDict
class AgentState(TypedDict, total=False):
request_id: str
purpose: str
page_id: str
current_url: str
step: str
challenge_detected: bool
challenge_attempts: int
challenge_status: Literal[
"not-needed", "pending", "resolved", "review", "denied"
]
error_code: str | None
final_assertion_passed: bool
Do not add the CapSolver credential, solution token, cookies, or raw page content. Store browser objects in an application-owned registry keyed by page_id; graph checkpoints should contain only the opaque identifier.
Authorization should run before any challenge tool:
from urllib.parse import urlparse
ALLOWED_HOSTS = {"staging.example.com", "research.example.com"}
ALLOWED_PURPOSES = {"qa-validation", "public-data-research"}
def authorize_challenge(state: AgentState) -> AgentState:
host = urlparse(state["current_url"]).hostname
attempts = state.get("challenge_attempts", 0)
if host not in ALLOWED_HOSTS:
return {**state, "challenge_status": "denied", "error_code": "domain"}
if state.get("purpose") not in ALLOWED_PURPOSES:
return {**state, "challenge_status": "denied", "error_code": "purpose"}
if attempts >= 2:
return {**state, "challenge_status": "review", "error_code": "retry-limit"}
return {**state, "challenge_status": "pending"}
This node is syntax-validated and independent of the model. In production, load policy from versioned configuration and reject unknown fields.
class BrowserRegistry:
def __init__(self):
self._pages = {}
def register(self, page_id: str, page) -> None:
self._pages[page_id] = page
def get(self, page_id: str):
if page_id not in self._pages:
raise KeyError("browser page is not registered")
return self._pages[page_id]
async def remove(self, page_id: str) -> None:
page = self._pages.pop(page_id, None)
if page is not None:
await page.close()
The registry prevents serialization of a Playwright Page and gives the host one place to enforce cleanup.
Redeem Your CapSolver Bonus Code
Boost your automation budget instantly!
Use bonus code CAP26 when topping up your CapSolver account to get an extra 5% bonus on every recharge — with no limits.
Redeem it now in your CapSolver Dashboard
CapSolver's Core SDK documents detect(page) and solve_on_page(page) for browser mode. The adapter below uses those methods and returns only a graph decision:
import os
from capsolver_core import create_capsolver
async def solve_turnstile_node(
state: AgentState,
registry: BrowserRegistry,
) -> AgentState:
page = registry.get(state["page_id"])
attempts = state.get("challenge_attempts", 0) + 1
async with create_capsolver(
api_key=os.environ["CAPSOLVER_API_KEY"],
default_timeout=120,
polling_interval=5,
) as cap:
detected = await cap.detect(page)
if not detected:
return {
**state,
"challenge_attempts": attempts,
"challenge_status": "not-needed",
"error_code": None,
}
results = await cap.solve_on_page(page)
failures = [item for item in results if item.error or not item.filled]
if failures:
return {
**state,
"challenge_attempts": attempts,
"challenge_status": "review" if attempts >= 2 else "pending",
"error_code": "fill-back-failed",
}
return {
**state,
"challenge_attempts": attempts,
"challenge_status": "resolved",
"error_code": None,
}
The code has been syntax-checked but not credential-run. A live test requires an approved page and secret. The graph never receives solution.token.
A filled token is an intermediate result. Verify the application's expected state:
async def verify_page_node(
state: AgentState,
registry: BrowserRegistry,
) -> AgentState:
page = registry.get(state["page_id"])
try:
await page.get_by_test_id("authorized-content").wait_for(timeout=15_000)
return {
**state,
"final_assertion_passed": True,
"step": "continue",
"error_code": None,
}
except Exception:
return {
**state,
"final_assertion_passed": False,
"challenge_status": "review",
"error_code": "page-assertion-failed",
}
Use an assertion owned by your application. Avoid selectors that expose personal or sensitive page content in logs.
from langgraph.graph import END, StateGraph
def route_after_authorization(state: AgentState) -> str:
if state["challenge_status"] == "pending":
return "solve"
if state["challenge_status"] in {"denied", "review"}:
return "human_review"
return "verify"
def route_after_solve(state: AgentState) -> str:
if state["challenge_status"] == "resolved":
return "verify"
if state["challenge_status"] == "pending":
return "authorize"
return "human_review"
def build_graph(authorize, solve, verify, human_review):
graph = StateGraph(AgentState)
graph.add_node("authorize", authorize)
graph.add_node("solve", solve)
graph.add_node("verify", verify)
graph.add_node("human_review", human_review)
graph.set_entry_point("authorize")
graph.add_conditional_edges(
"authorize",
route_after_authorization,
{"solve": "solve", "verify": "verify", "human_review": "human_review"},
)
graph.add_conditional_edges(
"solve",
route_after_solve,
{"authorize": "authorize", "verify": "verify", "human_review": "human_review"},
)
graph.add_edge("verify", END)
graph.add_edge("human_review", END)
return graph.compile()
The injected functions can close over the browser registry. Dependency injection makes policy and failure routes testable without a live service.
The reviewer should receive:
The reviewer should not receive the CapSolver credential or solution token. A state-changing action such as submission, purchase, account change, or message send should require its own authorization even after verification succeeds.
Unit tests can replace the solve node with deterministic stubs:
async def solved_stub(state: AgentState) -> AgentState:
return {
**state,
"challenge_attempts": state.get("challenge_attempts", 0) + 1,
"challenge_status": "resolved",
"error_code": None,
}
async def failed_stub(state: AgentState) -> AgentState:
return {
**state,
"challenge_attempts": state.get("challenge_attempts", 0) + 1,
"challenge_status": "review",
"error_code": "fixture-failure",
}
Test approved and denied domains, unsupported purposes, retry exhaustion, missing browser pages, a resolved challenge with a failed page assertion, and cleanup after terminal states.
Browser mode is appropriate when the agent already controls a Playwright page. Token mode can be simpler when your application knows the Turnstile page URL and public site key. CapSolver documents the AntiTurnstileTaskProxyLess task with required websiteURL and websiteKey, plus optional metadata.action and metadata.cdata.
Do not let the model invent these fields. Extract them deterministically from the approved page or application configuration.
Trace:
Do not trace prompts containing credentials, browser cookies, raw tokens, or unredacted form data. Define retention and access rules for screenshots and DOM evidence.
Confirm that the page finished loading, the browser uses the intended session, and the SDK version supports the challenge type. Treat an empty detection result as not-needed only when the page assertion can still pass.
Record the error category, compare parameters with current CapSolver documentation, and stop after the retry budget. Do not increase retries automatically.
Keep the same browser page, review callback or widget behavior, and verify that page navigation did not replace the context.
Store and enforce challenge_attempts. Route to human review after the configured limit.
Keep CAPTCHA recovery separate from the downstream action. The graph should surface the action's own error instead of re-solving the challenge.
A LangGraph Cloudflare Turnstile integration is most reliable when it behaves like a finite recovery workflow: detect, authorize, solve, verify, continue, or stop. The graph provides routing and observability; deterministic code provides policy; CapSolver provides the documented recognition layer.
Use CapSolver only for lawful, authorized automation. Review the current agent tools documentation, Core SDK guide, and related CapSolver blog tutorials before pinning an implementation.
Q: Does LangGraph solve Cloudflare Turnstile by itself?
No. LangGraph controls workflow state and routing; the CapSolver adapter calls the recognition service and browser methods.
Q: Should the solution token be returned to the model?
No. Apply it inside the controlled browser adapter and return only status, error category, and verification outcome.
Q: Which CapSolver method is used with Playwright?
The current Core SDK documents detect(page), get_captcha_info(page), and solve_on_page(page) for browser mode.
Q: How many retries should the graph allow?
Use a small explicit budget based on your workflow and route to review rather than allowing an unbounded loop.
Q: Can the agent call the recovery node for any URL?
No. Enforce a deterministic domain and purpose allowlist before the node can invoke CapSolver.
Q: What proves the challenge was handled successfully?
A page-level application assertion proves workflow recovery; a provider status or filled token alone is not sufficient.
Create a LangChain CAPTCHA solver agent tool with CapSolver, safe tool schemas, retry budgets, and verification for reCAPTCHA and Cloudflare Turnstile.

Build a Claude Computer Use CAPTCHA solver workflow with CapSolver guardrails, visual evidence IDs, policy checks, and reliable verification.
