CrewAI Screenshot Tool with RenderScreenshot

A tested CrewAI BaseTool that captures web pages with the RenderScreenshot API. Drop-in Python file, parameter mapping, retries and usage caps.

By Simon Chiu · Updated

Give a CrewAI agent a website_screenshot tool in one Python file. The tool is a BaseTool subclass that calls the RenderScreenshot API over HTTP, returns a public image URL with its size, retries only errors that can succeed on retry, and caps how many captures one run can make. No browser runs inside your crew.

This integration is part of our guide to giving AI agents web vision.

Last tested: 2026-10-15 with Python 3.12, crewai 1.15.28, pydantic 2.12.5. The first version of the tool used the Python SDK's take_json(); its logic ran against a stubbed client, and one real call with an invalid key checked the error path. SDK 1.0.0's take_json() does not send the Accept: application/json header that the API needs to return JSON, so the tool below calls the API with httpx instead. On 2026-10-18 we ran this httpx version (Python 3.14, httpx 0.28.1, pydantic 2.14.1) against a local mock that returns the API's JSON shape: a success, a 429 then a success, and a 401. That run used a stand-in BaseTool class, not crewai itself. The full crew was built but not run, because a run needs an LLM key.

What you can build

  • Pre-launch page review. A reviewer agent captures each landing page and reports whether the headline and the main button are above the fold, with the screenshot URL as evidence.
  • Competitor change log. A research crew captures a list of pricing pages every week and writes what changed, linking each capture.
  • Bug report enrichment. A triage agent receives a URL from a ticket, captures it on desktop and on mobile, and attaches both links to its summary.

Prerequisites

  • Python 3.10 to 3.13 (CrewAI 1.15.28 does not support 3.14 yet)
  • A RenderScreenshot API key in RS_API_KEY (create a free account)
  • An LLM key for CrewAI (for example OPENAI_API_KEY)
pip install "crewai==1.15.28" httpx

The tool calls the HTTP API directly, so it needs no SDK. The request body follows the POST endpoint reference.

The tool file

Save this as renderscreenshot_tool.py.

"""CrewAI tool that captures web pages with the RenderScreenshot API."""
from __future__ import annotations

import os
import time
from typing import Literal, Optional, Type

import httpx
from crewai.tools import BaseTool
from pydantic import BaseModel, Field, PrivateAttr

Preset = Literal['og_card', 'twitter_card', 'link_preview', 'full_page', 'mobile', 'desktop_hd']
RS_BASE_URL = os.environ.get('RS_BASE_URL', 'https://api.renderscreenshot.com')


class ScreenshotInput(BaseModel):
    url: str = Field(..., description='Public http(s) URL of the page to capture.')
    preset: Preset = Field('desktop_hd', description='Capture preset. Use full_page for long pages.')
    selector: Optional[str] = Field(None, description='CSS selector to capture one element instead of the page.')
    block_cookie_banners: bool = Field(True, description='Dismiss cookie consent banners before capture.')


class RenderScreenshotTool(BaseTool):
    name: str = 'website_screenshot'
    description: str = (
        'Capture a screenshot of a public web page and return a public image URL plus its size. '
        'Use it when you need to see how a page looks. It does not return the page text.'
    )
    args_schema: Type[BaseModel] = ScreenshotInput
    max_usage_count: Optional[int] = 20  # each call costs credits; cap it per run
    _http: httpx.Client = PrivateAttr()

    def __init__(self, api_key: Optional[str] = None, **kwargs):
        super().__init__(**kwargs)
        # Accept: application/json makes the API return JSON with a CDN link instead of image bytes.
        self._http = httpx.Client(
            base_url=RS_BASE_URL,
            headers={
                'Authorization': f"Bearer {api_key or os.environ['RS_API_KEY']}",
                'Accept': 'application/json',
            },
            timeout=60.0,
        )

    def _run(self, url: str, preset: str = 'desktop_hd', selector: Optional[str] = None,
             block_cookie_banners: bool = True) -> str:
        body = {'url': url, 'preset': preset, 'output': {'format': 'webp'}}
        if selector:
            body['capture'] = {'mode': 'element', 'selector': selector}
        if block_cookie_banners:
            body['block'] = {'cookie_banners': True}

        for attempt in range(3):
            resp = self._http.post('/v1/screenshot', json=body)
            if resp.status_code == 200:
                shot = resp.json()
                image, cache = shot['image'], shot.get('cache', {})
                return (f"Screenshot of {url}: {image['url']} "
                        f"({image.get('width')}x{image.get('height')}, cached={cache.get('hit')})")
            # Retry only throttling, timeouts and server errors. A 4xx such as a bad key
            # will not succeed on a second try, so never retry it.
            transient = resp.status_code in (408, 429) or resp.status_code >= 500
            if transient and attempt < 2:
                time.sleep(int(resp.headers.get('retry-after', 2 ** attempt)))
                continue
            try:
                error = resp.json().get('error', {})
            except ValueError:
                error = {}
            # Return the error as text so the agent can change its plan.
            return (f"Screenshot failed ({error.get('code', 'unknown')}, HTTP {resp.status_code}): "
                    f"{error.get('message', resp.text[:200])}")
        return 'Screenshot failed after 3 attempts.'

Design choices, and why:

  • A narrow preset list. The agent picks from six presets instead of free-form width and height, so every capture has a known size and a known cost.
  • Your code owns the risky options. Cookies, headers and auth are not in the schema. If a page needs a login, set it in _run, never from model input.
  • Errors come back as text. CrewAI passes the string to the agent, which can try another URL or report the failure.

Use it in a crew

"""Example crew: a visual reviewer that captures pages and reports what it sees."""
import os

from crewai import Agent, Crew, Process, Task

from renderscreenshot_tool import RenderScreenshotTool

screenshot = RenderScreenshotTool()  # reads RS_API_KEY

reviewer = Agent(
    role='Landing page reviewer',
    goal='Check that each landing page shows its headline and primary button above the fold',
    backstory='You review marketing pages before launch and report problems with evidence.',
    tools=[screenshot],
    llm=os.environ.get('CREW_LLM', 'gpt-4o-mini'),
    allow_delegation=False,
)

review = Task(
    description=(
        'Capture {url} with the desktop_hd preset. Report whether the headline and the main '
        'call-to-action button are visible in the first screen. Include the screenshot URL.'
    ),
    expected_output='A short verdict (pass or fail), the reasons, and the screenshot URL.',
    agent=reviewer,
)

crew = Crew(agents=[reviewer], tasks=[review], process=Process.sequential)

if __name__ == '__main__':
    print(crew.kickoff(inputs={'url': 'https://example.com'}))

A text-only model cannot look at the image behind the URL. For the "is the button visible" judgment, give the reviewer a vision-capable model, or add a step that sends the image URL to one. The capture itself works with any model.

Parameter mapping

Tool argument API field Notes
url url Must be a public http(s) URL
preset preset Sizes in the presets reference: og_card 1200x630, desktop_hd 1920x1080, mobile 390x844
selector capture.mode: "element" + capture.selector One element instead of the viewport
block_cookie_banners block.cookie_banners On by default here
(fixed) output.format: "webp" Smaller files than PNG for photos and UI
(fixed) Accept: application/json header JSON response with image.url, image.width, image.height and cache.hit
(fixed) httpx.Client(timeout=60.0) HTTP timeout in seconds

Errors, retries and limits

Situation What the tool does
HTTP 429 (rate limit) Waits Retry-After seconds (or 1 s, then 2 s) and retries, up to 3 attempts
HTTP 408 or 5xx Same backoff and retry
HTTP 400, 401, 403, 422 Returns the error text at once; no retry
21st call in one run CrewAI blocks it: "has reached its usage limit"

We tested these paths with the first, SDK-based version of the tool. A stubbed 429 followed by success made two calls and returned the URL. A real call with an invalid key came back as text in 0.7 s with no retries. With max_usage_count=1, CrewAI refused the second call and returned a usage_limit message to the agent.

One finding from the live call: SDK 1.0.0 reported the 401 as code internal_error and put the API's whole error object in message. Its retryable flag also said True. SDK 1.0.2 reads the nested error object correctly. The tool still decides on retries from the HTTP status, which works with every SDK version.

Each capture uses credits; the API reports the amount in the X-Credits-Used header (see response headers). Rate limits depend on your plan (see rate limits); keep max_usage_count below what one run should ever need.

Troubleshooting

Symptom Cause Fix
KeyError: 'RS_API_KEY' Key not in the environment export RS_API_KEY=rs_live_... or pass api_key=
KeyError: 'image' on success The request did not ask for JSON Keep the Accept: application/json header on the client
CrewAI install fails on Python 3.14 crewai 1.15.28 requires < 3.14 Use Python 3.12 or 3.13
"Screenshot failed (..., HTTP 401)" Wrong or revoked key Check the key in the dashboard
Agent describes an image it never saw Text-only LLM guessing from the URL Use a vision model for visual judgments
Agent stops after N captures max_usage_count reached Raise it, or split the work into several runs
Element capture shows the whole viewport Selector not found Pass a selector that exists on the page

Frequently asked questions

Does CrewAI have a built-in screenshot tool?
CrewAI's tool system lets you add any capability as a BaseTool subclass. This page gives you a tested subclass that calls the RenderScreenshot API, so your agents get a screenshot tool without running a browser in your process.
Why does the tool return a URL instead of the image?
CrewAI tools return text to the agent. A public image URL is short, works in the final report, and can be passed to a vision-capable step later. Returning base64 image data as text would cost thousands of tokens and the model could not see the image.
How do I stop an agent from spending too many credits?
Set max_usage_count on the tool. In our test, CrewAI refused the second call when the limit was 1 and returned a usage-limit message to the agent.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.