The obvious way to give a CrewAI agent eyes is to write a tool that calls a screenshot API and returns the image. It is the first thing everybody tries, it produces no error, and it does not work.
The reason is structural rather than a bug: a CrewAI tool's return value reaches the agent as text. CrewAI's own documentation is unambiguous — "If you do not override format_output_for_agent, typed outputs are sent to the agent as JSON. Plain string results work as before." Return raw PNG bytes from _run and something gets stringified into the conversation, the agent dutifully narrates whatever it makes of it, and the run is recorded as a success.
Images enter a crew through a completely different door. This post is about that door, and about the two defaults that will bite you afterwards.
The short answer
Capture the screenshot before the crew runs, and pass it in as a file with input_files, not as a tool result. Set multimodal=True on the agent and use a model that actually supports vision. Then turn off tool caching if the crew is meant to compare two captures of the same page, because CrewAI caches tool results by default and will hand you the first one twice.
Install what you actually need
Two packages, and the file support is a separate optional extra that most tutorials forget:
pip install 'crewai[tools]' 'crewai[file-processing]' site-shot
CrewAI requires "Python >=3.10 and <3.14". This is worth checking before anything else — a machine that has moved on to 3.14 fails at install, well before any screenshot code runs, with an error that says nothing about screenshots.
CrewAI itself flags the file package as new: "The file processing API is currently in early access." Treat the surface below as something to pin a version against.
The path that works
Capture first, hand the bytes to the crew, let the agent look:
from crewai import Agent, Task, Crew
from crewai_files import ImageFile, FileBytes
from site_shot import SiteShot
client = SiteShot() # reads SITESHOT_API_KEY from the environment
png = client.capture(
"https://example.com/", full_size=True, no_ads=True, no_cookie_popup=True
)
shot = ImageFile(source=FileBytes(data=png, filename="capture.png"))
reviewer = Agent(
role="Landing page reviewer",
goal="Spot layout and messaging problems a visitor would notice immediately",
backstory="You review pages the way a first-time visitor sees them.",
multimodal=True,
)
task = Task(
description="Review the landing page in {shot}. Describe what is above the fold, "
"and flag anything broken, cut off, or unreadable.",
expected_output="A short list of concrete problems, most serious first.",
agent=reviewer,
input_files={"shot": shot},
)
Crew(agents=[reviewer], tasks=[task]).kickoff()
Two details in there are easy to miss.
The file is referenced by key name inside the prompt. {shot} in the task description is the same "shot" key as in input_files. That is how the agent knows which file you mean when there is more than one.
You do not base64 anything. CrewAI decides how to ship the file: "You don't need to manage this yourself. CrewAI automatically uses the most efficient method based on file size and provider capabilities." Small files go inline as base64, larger ones through a provider's file-upload API.
input_files can also be set on the crew or the flow, and CrewAI states the precedence plainly: "Flow input_files < Crew input_files < Task input_files". Task-level wins.
What this means for agent design
Here is the honest consequence, and it is the thing to take away.
CrewAI's supported shape is capture first, then reason — you decide what to screenshot, and the crew analyses it. It is not "hand the agent a camera and let it decide what to look at mid-run". Every documented entry point for input_files is a kickoff or a task definition, which means the files are bound before the work starts.
That is fine for most real jobs: review these landing pages, compare our page to a competitor's, check these twenty client sites for a broken hero image. All of those know their URLs up front.
If you genuinely need the agent to choose targets as it goes, a tool can still capture and describe — returning text such as "captured, the hero image is missing" — which keeps the loop working even though the picture itself never reaches the model. Just do not pretend the agent is seeing it. If seeing it mid-loop is the requirement, LangChain handles that case more directly, and we wrote up how a LangChain tool returns an image.
Two defaults that will bite you
Tool caching is on. The Agent parameter table gives cache as "Enable caching for tool usage. Default is True."
For screenshots this is close to a trap. A crew whose job is "capture the page, wait, capture it again, tell me what changed" will call the same tool with the same arguments twice and get the first result back both times. It will then confidently report that nothing changed. Override the cache behaviour per tool, or vary the arguments, if re-capturing is the point.
A failed capture is recorded as a success. When a tool call raises and the error text goes back as an ordinary result, the agent narrates the problem in its final answer and the run is marked successful. Your crew "worked". You get a paragraph explaining that the screenshot could not be taken, in the place where you expected a page review — and nothing in the run status distinguishes that from a real answer. If the crew feeds anything downstream, check for the outcome you wanted rather than for a completed run.
multimodal=True is not the whole story
The flag defaults to False, and turning it on is necessary but not sufficient. CrewAI says it twice: "Ensure your language model supports multimodal capabilities" and "Ensure the selected LLM supports image inputs."
A text-only model will accept everything you have set up and produce a confident answer about an image it never received. Pick a vision-capable model explicitly.
Two related pieces of housekeeping:
VisionToolis not what its name suggests. CrewAI describes it as: "This tool is used to extract text from images." It is OCR, and it is hardwired toOPENAI_API_KEY. Asking it "is this layout broken?" gets you a dump of the page's text.- Mind the import roots.
from crewai.tools import BaseTool, toolis the framework, for writing your own tool.from crewai_tools import ...is the separate pre-built tools package. They look interchangeable and are not; mixing them up is the most common setup error in this framework.
Provider image limits are the real ceiling
The number that constrains a screenshot crew is not CrewAI's — it is the model provider's, and they differ enough to matter. From CrewAI's file documentation:
| Provider | Image limit |
|---|---|
| Anthropic | Max 5 MB, max 8000x8000 pixels, up to 100 images |
| OpenAI | Max 20 MB, up to 10 images per request |
| Google Gemini | Max 100 MB |
| AWS Bedrock | Max 4.5 MB, max 8000x8000 pixels |
A full_size=True capture of a long page can pass 5 MB and can certainly pass 8000 pixels tall. On Anthropic or Bedrock that is a hard stop, not a degradation — and CrewAI notes that an unsupported file type raises UnsupportedFileTypeError rather than falling back.
So size the capture to the provider:
- Drop
full_sizeand passwidthandheightfor a viewport shot. Most "how does this page look" questions are about the top of it anyway. max_heightcaps a full-page capture before it reaches an 8000-pixel provider limit.format="jpeg"on photo-heavy pages; keep PNG where the page is mostly text and UI.
Capturing from another country
Pass a two-letter ISO 3166-1 country code and the render routes through a real IP address in that country, with the language, time zone and geolocation defaults that belong to it:
png = client.capture(
"https://example.com/", country="DE", strict_country=True, no_ads=True
)
The code, never the name — country="DE", not country="Germany".
Use strict_country=True in a crew. Without it, an exhausted country pool falls back to a US render and returns a screenshot that looks entirely normal; your agent will then describe the US page in fluent detail with no indication anything went wrong. With it the SDK raises CountryUnavailableError (importable from site_shot) in your own code, before the crew ever starts — wrap the capture in try / except CountryUnavailableError, which is the right place to catch it. The API's raw "error": "country_unavailable" string is on the exception's body.
Geotargeting comes with any paid plan. The current country list is on the countries page and moves with live proxy capacity.
Where the honest limits are
Do not pass a screenshot URL that carries your API key. CrewAI can take a URL as a file source and pass it straight to the model provider — "URL Reference | Direct URL passed to the model". A Site-Shot capture URL contains userkey=, so handing that URL over sends your key to OpenAI, Anthropic or Google. Fetch the bytes yourself and pass FileBytes, as in the example above.
A screenshot is untrusted content. It is a picture of a page you do not control, shown to a model that follows instructions. A page can render text aimed at the model rather than the reader, and the capture delivers it faithfully. Do not put an agent that captures arbitrary URLs in the same loop as credentials or an outbound tool.
Pages behind a login will not capture. The API renders public URLs.
There is no HTTP-request tool in CrewAI. No built-in node to reach for — any call to an external API is a BaseTool subclass you write, or plain code before kickoff as above.
CodeInterpreterTool has been removed from crewai-tools, along with the deprecated allow_code_execution and code_execution_mode agent options. Older tutorials that shell out through it no longer run.
The free browser tool has no API. site-shot.com captures any public page with no signup, which is the quickest way to see what your target actually looks like before you build the crew. It is a browser tool, not a free API tier; the API needs a key, and plans start at $5/mo for 2,000 screenshots.
FAQ
Can a CrewAI tool return an image? No. A tool's result reaches the agent as text — CrewAI sends typed outputs to the agent as JSON unless you override format_output_for_agent, and plain string results work as before. To give an agent an image, pass it as a file through input_files on a task, crew or flow rather than returning it from a tool.
How do I give a CrewAI agent a website screenshot? Capture the image in your own code before the crew runs, wrap the bytes in an ImageFile with a FileBytes source, and pass it through input_files on the task, referencing it by key name inside the task description. Set multimodal to True on the agent and use a model that supports image inputs.
Why does my CrewAI crew report that a page has not changed when it clearly has? Because tool caching is enabled by default — the Agent cache parameter defaults to True — so a second call to the same tool with the same arguments returns the first result. A crew that captures the same URL twice to compare them gets the same image back both times. Override the caching behaviour or vary the arguments.
Why does my CrewAI screenshot crew succeed but return no analysis? Most often because the agent's model is not vision-capable. Setting multimodal to True is necessary but not sufficient — CrewAI requires that the selected language model supports image inputs, and a text-only model will produce a confident answer about an image it never received.
Is it safe to pass a screenshot API URL to CrewAI as an image source? No, if the URL contains your API key. CrewAI can pass a URL reference directly to the model provider, so a capture URL carrying userkey sends your key to OpenAI, Anthropic or Google along with the request. Fetch the image bytes in your own code and pass them as FileBytes instead.
The free Site-Shot browser tool captures any public page with no signup — try your target URL before you build the crew. For the API, plans and pricing start at $5/mo for 2,000 screenshots.