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 through our Python SDK, 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, renderscreenshot 1.0.0 (Python SDK from GitHub), pydantic 2.12.5. The tool's logic ran against a stubbed API client, and one real HTTP call to the API checked the error path with an invalid key. 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" "git+https://github.com/Render-Screenshot/rs-python.git"

The Python SDK is not on PyPI yet, so it installs from GitHub. See the Python SDK docs.

The tool file

Save this as renderscreenshot_tool.py. It is the exact file we tested.

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

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

from crewai.tools import BaseTool
from pydantic import BaseModel, Field, PrivateAttr
from renderscreenshot import Client, RenderScreenshotError, TakeOptions

Preset = Literal['og_card', 'twitter_card', 'link_preview', 'full_page', 'mobile', 'desktop_hd']


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
    _client: Client = PrivateAttr()

    def __init__(self, api_key: Optional[str] = None, **kwargs):
        super().__init__(**kwargs)
        self._client = Client(api_key or os.environ['RS_API_KEY'], timeout=60.0)

    def _run(self, url: str, preset: str = 'desktop_hd', selector: Optional[str] = None,
             block_cookie_banners: bool = True) -> str:
        options = TakeOptions.url(url).preset(preset).format('webp')
        if selector:
            options = options.element(selector)
        if block_cookie_banners:
            options = options.block_cookie_banners()

        for attempt in range(3):
            try:
                shot = self._client.take_json(options)
                return (f"Screenshot of {url}: {shot.get('url')} "
                        f"({shot.get('width')}x{shot.get('height')}, cached={shot.get('cached')})")
            except RenderScreenshotError as e:
                # 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 = e.http_status in (408, 429) or e.http_status >= 500
                if transient and attempt < 2:
                    time.sleep(e.retry_after or 2 ** attempt)
                    continue
                # Return the error as text so the agent can change its plan.
                return f'Screenshot failed ({e.code}, HTTP {e.http_status}): {e.message}'
        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 SDK call API field Notes
url TakeOptions.url(url) url Must be a public http(s) URL
preset .preset(...) preset Sizes in the presets reference: og_card 1200x630, desktop_hd 1920x1080, mobile 390x844
selector .element(selector) capture.selector One element instead of the viewport
block_cookie_banners .block_cookie_banners() block.cookie_banners On by default here
(fixed) .format('webp') output.format Smaller files than PNG for photos and UI
(fixed) Client(..., timeout=60.0) n/a SDK 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 each path. 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. That is why the tool decides on retries from http_status, not from the SDK's flag.

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=
pip cannot find renderscreenshot SDK not on PyPI yet Install from the GitHub URL above
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.