How to Build LangGraph Integrations with the You.com Web Search API

TLDR: A LangGraph search integration is a node you can cap, retry, and test. This guide builds a bounded plan-and-search loop on a StateGraph with the You.com Web Search API, then covers what LangGraph's runtime does to that node: its default retry policy skips urllib's error types, a retried node runs again from the top, node timeouts need async code, and parallel fan-out meets a default limit of 10 requests per second.
LangGraph is the runtime layer under LangChain. LangChain's own documentation lists durable execution, streaming, human-in-the-loop, and persistence as the runtime's value, and says LangChain is built on LangGraph without requiring you to know it. That shapes this page. If a model should decide when to search, an agent built with create_agent already runs on LangGraph, and our guides to the official LangChain search tool and to LangChain's agent documentation cover that path, including middleware for retries and call limits. This guide covers the other path: a graph you wire yourself, where search is a node and the runtime's policies apply to it directly.
When Does a Search Integration Need a StateGraph?
LangChain's guidance is to use LangGraph when you need fine-grained control over orchestration, durable execution for long-running stateful agents, or workflows that combine deterministic and agentic steps. Three web search situations meet that bar. The first is a fixed pipeline, such as plan, search, then write, where your code and not the model decides how many searches happen. The second is a job that must survive a crash or restart partway through. The third is fan-out, where several queries run in parallel and merge into one state. If none of those applies, a single tool inside create_agent is simpler, and the LangChain agents guide above maps the entry points.
How Do You Build a Bounded Search Loop in LangGraph?
The graph below has three nodes. The plan node returns the next query, or an empty list when the evidence is enough. The search node calls the You.com Web Search API with one query. The synthesize node writes the answer. A conditional edge after plan sends the run to search or to synthesize, and a static edge returns from search to plan. The planner and synthesizer are plain functions you pass in, so you can supply a model call or, as the placeholders do, a rule. That keeps the wiring independent of any model provider.
import json
import os
import socket
import urllib.error
import urllib.request
from typing import Annotated, Callable, TypedDict
from langgraph.graph import END, START, StateGraph
from langgraph.types import RetryPolicy
SEARCH_URL = "https://ydc-index.io/v1/search"
MAX_SEARCHES = 3 # first cap: how many searches one run may spend
def add_sources(old: list, new: list) -> list:
"""Reducer: append new sources, skipping URLs already in state."""
seen = {s["url"] for s in old}
return old + [s for s in new if s["url"] not in seen]
class State(TypedDict):
question: str
queries: list[str]
searches: int
sources: Annotated[list[dict], add_sources]
answer: str
def search(query: str, count: int = 5) -> list[dict]:
"""Call the You.com Web Search API and keep only what the model needs."""
req = urllib.request.Request(
SEARCH_URL,
data=json.dumps({"query": query, "count": count}).encode(),
headers={"Content-Type": "application/json", "X-API-Key": os.environ["YDC_API_KEY"]},
method="POST",
)
with urllib.request.urlopen(req, timeout=20) as resp:
results = json.load(resp).get("results", {})
sources = []
for hit in results.get("web", []) + results.get("news", []):
if not hit.get("url"): # every result field is optional in the schema
continue
snippet = (hit.get("snippets") or [hit.get("description") or ""])[0]
sources.append({"title": hit.get("title", ""), "url": hit["url"], "snippet": snippet})
return sources
def is_transient(exc: BaseException) -> bool:
"""Retry network failures, timeouts, 429 and 5xx. Fail fast on other 4xx (401, 402, 422)."""
if isinstance(exc, urllib.error.HTTPError):
return exc.code == 429 or exc.code >= 500
return isinstance(exc, (urllib.error.URLError, ConnectionError, socket.timeout, TimeoutError))
def build_graph(plan: Callable[[State], list], synthesize: Callable[[State], str]):
"""plan returns the next queries (an empty list means enough evidence); synthesize returns the answer."""
def plan_node(state: State) -> dict:
return {"queries": plan(state)}
def route_after_plan(state: State) -> str:
if state["queries"] and state["searches"] < MAX_SEARCHES:
return "search"
return "synthesize"
def search_node(state: State) -> dict:
return {"sources": search(state["queries"][0]), "searches": state["searches"] + 1}
def synthesize_node(state: State) -> dict:
return {"answer": synthesize(state)}
builder = StateGraph(State)
builder.add_node("plan", plan_node)
builder.add_node("search", search_node, retry_policy=RetryPolicy(max_attempts=3, retry_on=is_transient))
builder.add_node("synthesize", synthesize_node)
builder.add_edge(START, "plan")
builder.add_conditional_edges("plan", route_after_plan, {"search": "search", "synthesize": "synthesize"})
builder.add_edge("search", "plan")
builder.add_edge("synthesize", END)
return builder.compile()
def first_pass_plan(state: State) -> list:
"""Placeholder planner: search the question once. Replace with a model call."""
return [] if state["searches"] else [state["question"]]
def cite_sources(state: State) -> str:
"""Placeholder synthesizer: list the sources. Replace with a model call that cites [n]."""
return "\n".join("[%d] %s %s" % (i, s["title"], s["url"]) for i, s in enumerate(state["sources"], 1))
if __name__ == "__main__":
graph = build_graph(first_pass_plan, cite_sources)
result = graph.invoke(
{"question": "What changed in LangGraph recently?", "queries": [], "searches": 0, "sources": [], "answer": ""},
config={"recursion_limit": 12}, # the default is 10,007 super-steps in current releases
)
print(result["answer"])
State holds the question, the pending queries, a search counter, the accumulated sources, and the answer. Each state key has its own reducer, and per the graph API documentation a key without one is overwritten by the latest update. The sources key gets a reducer that appends and drops duplicate URLs, so a second pass cannot repeat a page. The search function keeps only title, URL, and snippet. Every field of a web or news result in the Web Search API response schema is optional, news results carry no snippets, and snippets are omitted when you ask for highlights, so the function skips hits without a URL and falls back to the description.
The loop has two caps, and they do different jobs. MAX_SEARCHES is a business rule held in state: the routing function stops sending the run to search after three searches, even if the planner keeps asking. The recursion limit is the runtime's backstop. It counts super-steps, and nodes that run sequentially belong to separate super-steps. Here a full run is four planner passes, three searches, and one synthesis, which comes to eight super-steps if you count each sequential node as one, so the invoke call sets a limit of 12 to leave headroom. The default for a graph you compile yourself is 10,007 steps in current releases, which would let a stuck loop spend thousands of paid searches before it stopped. The graph API documentation gives 1000 as the default from langgraph 1.0.6 on, but the released source set 10,000 in that version and 10,007 from 1.1.4. Pass recursion_limit as its own key in the config, not inside configurable, and expect GraphRecursionError when it trips.
Which Runtime Policies Apply to a Search Node?
Most of what makes a search node safe in production is configuration on the graph, not code in the node. This table lists the policies this page covers and the detail that most often surprises people, all from the LangGraph documentation and source code.
| Policy | Where you set it | What to know |
|---|---|---|
| Retry | retry_policy on add_node | The default skips several exception families, including OSError and RuntimeError |
| Timeout (langgraph 1.2 or later) | timeout on add_node | Applies to async nodes only; a sync node with a timeout is rejected at compile time |
| Error handler (langgraph 1.2 or later) | error_handler on add_node | Runs after retries are exhausted; Command routing adds to static edges instead of replacing them |
| Recursion limit | recursion_limit in the invoke config | 10,007 super-steps by default in current releases (the docs say 1000); a top-level key, not under configurable |
| Concurrency | max_concurrency in the invoke config | Caps simultaneous tasks in a parallel step; also a top-level key |
| Persistence | checkpointer at compile time, thread_id in configurable | Checkpoints land at super-step boundaries, and an interrupted node runs again from its first line |
Why Does the Default Retry Policy Skip urllib Errors?
Pass a RetryPolicy to add_node and LangGraph re-runs a failed attempt with exponential backoff: three attempts, a half-second first interval, a doubling factor, and jitter by default. The surprise is what counts as retryable. The fault tolerance documentation says the default retries any exception except a list that includes ValueError, TypeError, LookupError, RuntimeError, and OSError. In Python, urllib's URLError and HTTPError are OSError subclasses, and so is socket.timeout, so by that documented rule a search node written with the standard library is not retried on a 503 response or a URLError unless you say so. Test it on your version, as the checklist below suggests. For httpx, the same page says the default retries only 5xx responses, which leaves 429 out.
The example overrides retry_on with a predicate that follows the You.com documentation: rate limits and server errors are transient, while 401, 402, and 422 are not. The Rate Limits documentation says to honor the Retry-After header on a 429 when present and recommends exponential backoff with a cap. The documented RetryPolicy parameters are maximum attempts, initial interval, backoff factor, maximum interval, jitter, and the retry predicate, and none of them reads Retry-After. If your account runs close to its limit, lower concurrency or sleep for the header's value inside the node before you raise. Failing fast matters as much as retrying. A missing key raises KeyError in the example, which is a LookupError that the predicate does not retry, and a 401 or 402 surfaces at once instead of spending three attempts.
What Happens When a Search Node Runs Twice?
The graph API documentation is direct about re-execution. With a checkpointer, LangGraph saves checkpoints at super-step boundaries, not in the middle of a function. When execution resumes after an interrupt or a retry, the affected node runs again from the start of its function, and code and side effects before the pause run again. For a search node, that means a retried call can reach the API twice, and any other side effect in the same node, such as a database write or a notification, repeats with it. Keep the search call in a node of its own. Give any write elsewhere an idempotency key or an upsert, as the documentation advises.
Persistence covers the other half. Compile with a checkpointer, pass a thread_id in the configurable part of the config, and a run that stops partway resumes from saved state instead of starting over. The checkpointer documentation adds that when a node fails mid-super-step, LangGraph stores pending writes from the nodes that completed. InMemorySaver is for development. Persistent savers ship as separate packages, langgraph-checkpoint-sqlite and langgraph-checkpoint-postgres.
How Do You Search in Parallel Without Tripping Rate Limits?
The serial loop is easy to reason about and slow. To run several queries at once, return a list of Send objects from the routing function, one per query. The graph API documentation describes this map-reduce pattern, and the targets run in parallel in the same super-step. Two details change. Parallel branches that write the same key need a reducer, which is why add_sources exists. Without one, LangGraph raises an INVALID_CONCURRENT_GRAPH_UPDATE error when two branches update the key in the same super-step. That includes the searches counter if each branch updates it, so give it a reducer such as operator.add and have each branch return 1. And the Web Search API's default for self-serve accounts is 10 requests per second, so a fan-out of 15 queries can return 429 responses. Our Web Search API guide covers the endpoint, pricing, and limits. Set max_concurrency at the top level of the invoke config to cap simultaneous tasks. It limits bursts but is not a rate limiter, so keep the retry predicate that treats 429 as transient. The documentation notes that only failing branches are retried, so one rejected query does not re-run the other fourteen.
Where Do Timeouts and Error Handlers Fit?
Two more policies live on add_node, and both require langgraph 1.2 or later. The timeout caps a single attempt and raises NodeTimeoutError, which the retry policy treats as retryable by default. It applies only to async nodes, and the documentation tells you to wrap blocking I/O in asyncio.to_thread inside an async node. The example's search node is synchronous, so its guard is the 20-second timeout on urlopen itself.
The error handler runs after retries are exhausted. It receives the state and a NodeError, and it can update state or route with a Command. Use it where degrading beats failing, for example to write an answer from the sources already gathered and say that search failed. One documented trap applies: Command adds dynamic routing on top of static edges, so a node that returns a Command with goto and also has a static edge runs both targets. Give each node either static edges or Command routing. We kept the handler out of the example because failure routing deserves a test against your own graph. Without one, an exhausted retry policy raises and the run fails, which is the right default when a revoked key or an empty balance needs a human.
How Do You Test the Graph Without Calling the API?
Keep the search function, the retry predicate, and the reducer as plain functions so they test without LangGraph, and pass the planner and synthesizer in so a test can script them. We tested this file in three layers. It compiles. Twenty-three offline tests cover the search function against a local mock of the documented response, including a hit with no URL, empty snippets, news results, and each error status, plus the retry predicate and the reducer. The same tests drive the graph against a small stand-in for the documented API that models sequential super-steps, reducers, conditional edges, retry policies, and the recursion limit. Through it we confirmed that the search cap holds when a planner never stops, that eight super-steps fit inside a limit of 12, that a limit of 5 raises, that a 503 is retried and counted once, and that a 401 is not retried.
The stand-in follows the documentation, so it checks this file's logic and not the real library. LangGraph was not installed in our test environment. Run the same scenarios against a real install before you depend on the wiring. LangChain's unit testing guide shows how to script a fake chat model and an in-memory checkpointer for the model side.
What Should You Check Before You Ship?
- Pin the langgraph version. Timeouts and error handlers need 1.2 or later, and the default recursion limit rose from 25 to 10,000 in 1.0.6 and to 10,007 in 1.1.4.
- Set recursion_limit on every invoke, and test that a planner that never stops hits your search cap first.
- Make the search node fail with a 503, a 401, and a timeout, and assert which of them are retried.
- Confirm that nothing else in the search node has a side effect that would repeat on a retry or a resume.
- Compile with a persistent checkpointer, stop the process mid-run, and resume with the same thread_id.
- Compare your fan-out width and max_concurrency with your account's rate limit, and read the X-RateLimit-Remaining header if you throttle on the client.
- Treat snippets in state as untrusted input before they reach the planner prompt. Our article on prompt injection and intent hijacking shows how a search result becomes a command.
Related Guides
- How to Add a Web Search Tool to a LangChain Agent With the You.com Web Search API
- LangChain Agents Documentation: create_agent, Middleware, and What Changed Since 0.3
- How to Add Web Search to Agent Tool Calling With the You.com Web Search API
- You.com Web Search API: Endpoint, Pricing, Limits
- Prompt Injection and Intent Hijacking in AI Agents
LI Test
LI Test
Share Article:
Related resources.

What Is MCP Web Search? Live Results for Model Context Protocol Agents
October 12, 2026
Blog

How to Add Web Search to AutoGen Agents With the You.com MCP Server
October 2, 2026
Blog

How to Add Web Search to Flowise Agents With the You.com MCP Server
October 2, 2026
Blog


