ScreenshotRun plugs into LangChain as a custom tool. Your agent sends a URL. The LangChain screenshot API renders the page in a real browser and returns a base64 image ready for multimodal analysis. No Chromium binary to install, no browser lifecycle to manage. One HTTP call inside a Python function.
Why Agents Need Visual Context
Text-based scraping pulls raw HTML or parsed text from a page, but that output strips away everything visual. Rendered JavaScript content, chart libraries like D3 or Chart.js, CSS layouts, above-the-fold design, cookie banners covering content. None of that survives a text-only scrape. Your agent gets words without understanding what the user actually sees.
Multimodal models changed this equation. Claude and GPT-4o can look at a screenshot and describe the layout, identify broken elements, read text rendered inside images, spot visual regressions between two captures. A LangChain screenshot tool turns your agent from a text processor into something closer to a user who can actually open a browser and look at the page. The missing piece is getting that screenshot reliably, at scale, without bundling a headless browser into your agent's runtime.
Quick Start with the @tool Decorator
Giving your LangChain agent screenshot capabilities starts with the @tool decorator. This pattern wraps a plain Python function so the agent can call it by name, pass a URL, and get back image content blocks that multimodal models understand.
import base64
import requests
from langchain_core.tools import tool
SCREENSHOTRUN_API_KEY = "your_api_key_here"
@tool
def capture_screenshot(url: str) -> list[dict]:
"""Capture a screenshot of a web page. Returns the image for visual analysis."""
response = requests.get(
"https://api.screenshotrun.com/v1/screenshots/capture",
params={"url": url, "format": "png", "response_type": "image"},
headers={"Authorization": f"Bearer {SCREENSHOTRUN_API_KEY}"},
timeout=60,
)
response.raise_for_status()
image_base64 = base64.b64encode(response.content).decode("utf-8")
return [
{"type": "text", "text": f"Screenshot of {url}"},
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": image_base64,
},
},
]
About fifteen lines of actual logic. The function hits the ScreenshotRun API with a GET request, receives the rendered PNG as binary, then encodes it to base64. It returns a list of content blocks. That return format matters because it tells the model "here is text context, and here is an image to look at." More on that format in the multimodal section below.
If you already use the Python integration, this pattern will feel familiar. The only addition is wrapping the call inside LangChain's tool interface.
BaseTool Class for Production Agents
For prototyping, the @tool decorator works well. For production agents that need configurable API keys, retry logic, async support, or typed input validation, subclassing BaseTool gives you more control.
import base64
import httpx
from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field
class ScreenshotInput(BaseModel):
url: str = Field(description="The URL of the web page to capture")
full_page: bool = Field(default=False, description="Capture the entire scrollable page")
device: str = Field(default="desktop", description="Device type: desktop, mobile, or tablet")
class ScreenshotTool(BaseTool):
name: str = "capture_screenshot"
description: str = (
"Capture a rendered screenshot of a web page. "
"Use this when you need to visually inspect a website, "
"check its layout, or analyze visual content."
)
args_schema: type[BaseModel] = ScreenshotInput
api_key: str = ""
base_url: str = "https://api.screenshotrun.com/v1/screenshots/capture"
def _run(
self, url: str, full_page: bool = False, device: str = "desktop"
) -> list[dict]:
params = {
"url": url,
"format": "png",
"response_type": "image",
"full_page": str(full_page).lower(),
"device": device,
}
with httpx.Client(timeout=60) as client:
resp = client.get(
self.base_url,
params=params,
headers={"Authorization": f"Bearer {self.api_key}"},
)
resp.raise_for_status()
img_b64 = base64.b64encode(resp.content).decode("utf-8")
return [
{"type": "text", "text": f"Screenshot of {url} (device={device}, full_page={full_page})"},
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": img_b64,
},
},
]
async def _arun(
self, url: str, full_page: bool = False, device: str = "desktop"
) -> list[dict]:
params = {
"url": url,
"format": "png",
"response_type": "image",
"full_page": str(full_page).lower(),
"device": device,
}
async with httpx.AsyncClient(timeout=60) as client:
resp = await client.get(
self.base_url,
params=params,
headers={"Authorization": f"Bearer {self.api_key}"},
)
resp.raise_for_status()
img_b64 = base64.b64encode(resp.content).decode("utf-8")
return [
{"type": "text", "text": f"Screenshot of {url} (device={device}, full_page={full_page})"},
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": img_b64,
},
},
]
Pydantic's ScreenshotInput model validates inputs before the API call ever fires, so the agent can't pass an empty URL or an invalid device type. The _arun method uses httpx.AsyncClient for agents running inside async event loops, which is common in LangGraph workflows or FastAPI backends. Instantiate the tool with your key:
screenshot_tool = ScreenshotTool(api_key="your_api_key_here")
Multimodal Tool Responses
Returning a plain string from a tool works for text-only results, but screenshot tools need to pass actual image data back to the model. LangChain supports structured content blocks in tool responses. The format used in the examples above follows the pattern that both Claude and GPT-4o understand natively:
# Content blocks returned by the tool
[
{"type": "text", "text": "Screenshot of https://example.com"},
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": "iVBORw0KGgo...", # base64-encoded PNG
},
},
]
Claude processes image blocks in tool results without any extra configuration. When using ChatAnthropic, the content blocks pass through directly in the API call. For OpenAI models via ChatOpenAI, LangChain handles the translation to the expected format automatically.
Including a text block before the image gives the model context about what it is looking at. Include the URL, device type, or any parameters that affect interpretation. A full-page screenshot of a long landing page looks very different from a viewport-only capture, and the model needs that context to analyze accurately.
For workflows where you want the base64 data stored separately or passed between nodes, you can also return a dictionary with both the raw base64 string and the formatted content blocks. That flexibility becomes important in LangGraph pipelines.
Full Agent Example
Putting it all together: a complete, runnable example using the LangChain screenshot API that takes a screenshot and describes what it sees. This example uses Claude as the reasoning model, but swapping to GPT-4o requires only changing the model class.
import base64
import requests
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
SCREENSHOTRUN_API_KEY = "your_api_key_here"
@tool
def capture_screenshot(url: str) -> list[dict]:
"""Capture a screenshot of a web page and return it as an image for analysis."""
response = requests.get(
"https://api.screenshotrun.com/v1/screenshots/capture",
params={"url": url, "format": "png", "response_type": "image"},
headers={"Authorization": f"Bearer {SCREENSHOTRUN_API_KEY}"},
timeout=60,
)
response.raise_for_status()
img_b64 = base64.b64encode(response.content).decode("utf-8")
return [
{"type": "text", "text": f"Screenshot of {url}"},
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": img_b64,
},
},
]
model = ChatAnthropic(model="claude-sonnet-4-20250514")
model_with_tools = model.bind_tools([capture_screenshot])
result = model_with_tools.invoke([
HumanMessage(content="Take a screenshot of https://example.com and describe the page layout.")
])
print(result.content)
Run this script and the agent will call capture_screenshot on its own, receive the image, then generate a natural-language description of the page. The model decides when to use the tool based on the prompt — you don't need to explicitly trigger the screenshot step.
For agents that combine screenshots with other capabilities, just add more tools to the bind_tools list. A monitoring agent might have capture_screenshot alongside a text comparison tool and a notification sender. The model picks which tools to call based on the task.
Screenshots in LangGraph Workflows
Beyond single-agent use, a LangGraph screenshot tool node lets you build multi-step visual workflows as directed graphs. Instead of a single agent deciding everything, you define explicit nodes for each step. A screenshot-then-analyze pipeline fits this pattern naturally: one node captures the image, the next node feeds it to a model for analysis.
import base64
import operator
from typing import Annotated, TypedDict
import requests
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage
from langgraph.graph import StateGraph, START, END
SCREENSHOTRUN_API_KEY = "your_api_key_here"
class PipelineState(TypedDict):
url: str
screenshot_b64: str
analysis: str
messages: Annotated[list, operator.add]
def capture_node(state: PipelineState) -> dict:
response = requests.get(
"https://api.screenshotrun.com/v1/screenshots/capture",
params={"url": state["url"], "format": "png", "response_type": "image"},
headers={"Authorization": f"Bearer {SCREENSHOTRUN_API_KEY}"},
timeout=60,
)
response.raise_for_status()
img_b64 = base64.b64encode(response.content).decode("utf-8")
return {"screenshot_b64": img_b64}
def analyze_node(state: PipelineState) -> dict:
model = ChatAnthropic(model="claude-sonnet-4-20250514")
message = HumanMessage(
content=[
{"type": "text", "text": f"Analyze this screenshot of {state['url']}. Describe the layout, key elements, and any issues you notice."},
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": state["screenshot_b64"],
},
},
]
)
result = model.invoke([message])
return {"analysis": result.content}
graph = StateGraph(PipelineState)
graph.add_node("capture", capture_node)
graph.add_node("analyze", analyze_node)
graph.add_edge(START, "capture")
graph.add_edge("capture", "analyze")
graph.add_edge("analyze", END)
pipeline = graph.compile()
output = pipeline.invoke({"url": "https://example.com", "screenshot_b64": "", "analysis": "", "messages": []})
print(output["analysis"])
Each node handles one responsibility. The capture node talks to the ScreenshotRun API. The analyze node sends the image to Claude. You can extend this graph with additional nodes: a comparison node that diffs two screenshots over time, a reporting node that formats findings into markdown. Add a notification node that posts alerts to Slack, or a conditional edge that retries capture on failure. LangGraph's state management keeps the screenshot data flowing between nodes without global variables or manual plumbing.
This pattern works especially well for AI web analysis workflows that run on a schedule, processing dozens or hundreds of URLs through the same pipeline.
Useful Parameters for Agent Screenshots
The API accepts parameters beyond just a URL. For agent workflows, a subset of these are particularly relevant because they control what the screenshot captures and how efficiently the call runs.
| Parameter | Values | Agent use case |
|---|---|---|
full_page |
true / false |
Capture entire scrollable page for content analysis. See full-page screenshots |
device |
desktop, mobile, tablet |
Test responsive layouts across device types |
dark_mode |
true / false |
Verify dark mode rendering |
selector |
CSS selector string | Capture a specific element instead of the full viewport |
block_cookies |
true / false |
Remove cookie consent banners that obscure page content |
block_ads |
true / false |
Strip ads for cleaner visual analysis |
cache_ttl |
Seconds (e.g., 3600) |
Cache results to avoid redundant captures when agents retry or re-process URLs |
delay |
Milliseconds | Wait for JavaScript rendering, animations, or lazy-loaded content to finish |
format |
png, jpeg, webp |
JPEG with lower quality for faster transfers when pixel precision is not critical |
Pass these as query parameters in the API call. In the BaseTool example above, add them to the ScreenshotInput Pydantic model so the agent can set them dynamically based on the task. A visual QA agent might always use full_page=true with block_cookies=true, while a competitor monitoring agent cycles through device=desktop and device=mobile for each target URL.
API Call vs Local Playwright in Agent Context
You could skip the API and use Playwright directly inside your agent. Install playwright, download Chromium, launch a browser instance, navigate, capture, close. It works. But inside an agent loop, the tradeoffs become painful fast.
A Playwright browser process consumes 300 to 600 MB of memory per instance. If your agent processes ten URLs in sequence, that browser stays alive the entire time — or you pay the startup cost on each capture. In serverless environments like AWS Lambda or Cloud Functions, the Chromium binary alone pushes you past default deployment size limits. You end up managing browser pools, handling zombie processes when agents crash mid-run, debugging headless Chrome flags across different Linux distributions, upgrading Chromium versions when rendering breaks.
A LangChain screenshot API call through ScreenshotRun moves all of that off your infrastructure. One GET request, one HTTP response. The browser runs on the API side. Your agent's memory footprint stays small. Serverless deployment stays simple. The cache_ttl parameter means repeated captures of the same URL within a time window return instantly from cache, which matters when agents retry steps or when multiple agents in a pipeline hit the same page.
You do trade network latency and a per-capture cost. For agents that capture thousands of screenshots daily with no budget for API calls, self-hosted Playwright behind a dedicated server makes sense. For everything else — prototyping, moderate volume, serverless deployment, teams that don't want to maintain browser infrastructure, the API approach wins on development speed and operational simplicity.
What Agents Build with Screenshots
Visual QA agents run through a list of URLs after each deployment, capture screenshots across desktop and mobile viewports, then ask the model to flag layout shifts, overlapping elements, missing images, or broken styling. This catches regressions that automated test suites miss because the HTML is technically valid even when the page looks wrong to a human.
Competitor monitoring becomes substantially more useful with visual context. Instead of diffing raw HTML, which changes constantly due to A/B tests, personalization tokens, session IDs, an agent compares screenshots taken days apart. The model identifies meaningful changes: new pricing, redesigned hero sections, removed CTAs, added product features. Check brand monitoring for more on this pattern.
SEO audit agents use full-page screenshots to verify that above-the-fold content matches optimization targets. Does the H1 appear prominently? Is the CTA visible without scrolling? These are visual questions that text analysis cannot answer reliably.
Content verification agents screenshot pages after CMS publishes go live, comparing the rendered output against the intended design. Mismatched images, wrong fonts loading from a CDN, collapsed accordions, missing sections due to template errors — all visible in a screenshot, all invisible in an HTML diff. The page content verification use case covers this workflow in depth.
Defacement detection agents capture screenshots at regular intervals and flag drastic visual changes. When a site gets compromised, the visual difference between the legitimate page and the defaced version is immediate and obvious to a multimodal model. Parsing HTML for injected scripts takes longer and misses visual defacement entirely.
Report generation agents capture screenshots of dashboards, analytics pages, client websites, or internal tools and compile them into visual reports. Combined with the Page Analyze feature, the agent can extract structured data from the page alongside the screenshot, giving stakeholders both the numbers and the visual snapshot in one pass.
If you work with Claude specifically, the Claude MCP integration offers another path: connecting ScreenshotRun as a Model Context Protocol server that Claude Desktop or Claude Code can call directly, outside of LangChain entirely.
FAQ#
Wrap an HTTP call to a screenshot API inside a function decorated with @tool from langchain_core.tools. The function takes a URL, sends a GET request to the API endpoint, encodes the returned image as base64, and returns it as a list of content blocks (one text block with the URL, one image block with the base64 data). The model receives the image directly in the tool response.
Yes. LangChain supports multimodal content blocks in tool responses. Return a list containing a text block and an image block with base64-encoded data. Claude processes these image blocks natively through ChatAnthropic. For OpenAI models via ChatOpenAI, LangChain translates the format automatically.
No. A screenshot API handles browser rendering on remote infrastructure. Your LangChain tool makes a single HTTP GET request and receives the rendered image. This avoids installing Chromium, managing browser processes, dealing with driver version mismatches, or adding hundreds of megabytes to your deployment package.
Define a capture node that calls the screenshot API and stores the base64 result in the graph state. Connect it to an analyze node that passes the image to a multimodal model via a HumanMessage with image content blocks. LangGraph state management handles data flow between nodes automatically.
Yes. Both models accept image content blocks in tool responses. When using ChatAnthropic, the base64 image passes through directly. When using ChatOpenAI, LangChain converts the format. The tool code stays the same regardless of which model your agent uses.
Add the cache_ttl parameter to your API call. Setting cache_ttl=3600 tells the API to return a cached screenshot for the same URL within one hour instead of re-rendering. This reduces latency and API usage when agents revisit URLs or when multiple agents in a pipeline process the same page.