CAPSOLVER
Blog
How to Solve Cloudflare Turnstile in LangGraph Agents

How to Solve Cloudflare Turnstile in LangGraph Agents

Logo of CapSolver

Ethan Collins

Pattern Recognition Specialist

23-Jul-2026

TL;DR

  • A LangGraph Cloudflare Turnstile solver integration should model CAPTCHA handling as a controlled recovery node, not an unrestricted tool available on every route.
  • CapSolver documents capsolver-agent as the tool-adapter layer over capsolver-core, with LangChain tools for agent frameworks and browser methods for Playwright sessions.
  • Keep navigation decisions in the graph, deterministic authorization in application code, and recognition in the CapSolver service.
  • Preserve one browser session through detection, solving, fill-back, and final page assertion; never return the raw token to the model.
  • Use bounded retries, explicit terminal states, redacted tracing, and human review for state-changing or unclear actions.
  • The example graph below includes state definitions, policy routing, a CapSolver recovery node, failure handling, and verification.

What a LangGraph Cloudflare Turnstile integration should do

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.

Prerequisites and supported integration path

Use an isolated Python environment. CapSolver's current official guide installs the core and agent packages from GitHub:

bash Copy
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:

  • an owned or explicitly authorized target;
  • a domain allowlist;
  • a defined business purpose;
  • a browser session registry;
  • a maximum retry count;
  • a timeout budget;
  • a final page assertion;
  • a human-review rule for consequential actions.

Design the LangGraph state

Keep only non-secret operational data in the graph state:

python Copy
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.

Build a deterministic authorization node

Authorization should run before any challenge tool:

python Copy
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.

Register a browser session outside graph state

python Copy
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
Bonus Code

Implement the CapSolver recovery node

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:

python Copy
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.

Verify the page outcome

A filled token is an intermediate result. Verify the application's expected state:

python Copy
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.

Assemble the LangGraph workflow

python Copy
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.

Add a human-review node

The reviewer should receive:

  • request ID;
  • approved purpose;
  • hostname;
  • attempted action;
  • retry count;
  • redacted error category;
  • safe screenshot reference, if permitted;
  • proposed next step.

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.

Test the graph without calling external services

Unit tests can replace the solve node with deterministic stubs:

python Copy
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.

Use token mode when page parameters are known

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.

Observability without secret leakage

Trace:

  • graph node;
  • request ID;
  • policy decision;
  • hostname;
  • challenge type;
  • attempt number;
  • elapsed time;
  • error category;
  • fill-back boolean;
  • final assertion boolean.

Do not trace prompts containing credentials, browser cookies, raw tokens, or unredacted form data. Define retention and access rules for screenshots and DOM evidence.

Failure modes and fixes

No challenge detected

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.

Task fails before ready

Record the error category, compare parameters with current CapSolver documentation, and stop after the retry budget. Do not increase retries automatically.

Token fill-back fails

Keep the same browser page, review callback or widget behavior, and verify that page navigation did not replace the context.

Graph loops indefinitely

Store and enforce challenge_attempts. Route to human review after the configured limit.

Verification succeeds but business action fails

Keep CAPTCHA recovery separate from the downstream action. The graph should surface the action's own error instead of re-solving the challenge.

Production checklist

  • Pin dependency versions or commits.
  • Use separate staging and production policies.
  • Store secrets outside graph state.
  • Restrict domains and purposes.
  • Set per-call and total deadlines.
  • Preserve one browser session.
  • Redact tokens and cookies.
  • Verify the page after fill-back.
  • Add human review for consequential actions.
  • Test cleanup and cancellation.
  • Monitor denial, error, recovery, and assertion rates.
  • Review policy changes like code.

Conclusion: make challenge handling a graph recovery state

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.

FAQ

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.

Compliance Disclaimer: The information provided on this blog is for informational purposes only. CapSolver is committed to compliance with all applicable laws and regulations. The use of the CapSolver network for illegal, fraudulent, or abusive activities is strictly prohibited and will be investigated. Our captcha-solving solutions enhance user experience while ensuring 100% compliance in helping solve captcha difficulties during public data crawling. We encourage responsible use of our services. For more information, please visit our Terms of Service and Privacy Policy.

More