DEV Community

Cover image for Why screenshot-url Tool Calls Fail When Handling Heavy Vision Tasks
Crawler Bros
Crawler Bros

Posted on

Why screenshot-url Tool Calls Fail When Handling Heavy Vision Tasks

Exposing the Website Screenshot Generator (screenshot-url) to an AI agent via the Model Context Protocol (MCP) requires mapping the Actor input schema directly into structured tool signatures. When an agent invokes this tool to capture visual context for a vision model, the success of the integration depends on managing the technical gap between standard platform runs and the unique constraints of agentic loops.

By scoping the Apify MCP server using the ?tools= configuration parameter, you can isolate the screenshot-url schema. This prevents token bloat and ensures the agent focuses on relevant parameters like selectorsToHide and waitUntilNetworkIdleAfterScroll. However, moving from simple web captures to processing heavy screenshot outputs introduces architectural hurdles involving synchronous 300 second caps and the transient nature of unnamed storage.

Checked against the Actor's input schema and Apify docs on 2026-09-20.

How do you connect screenshot-url to an MCP agent?

Connecting the Actor to an agent requires configuring the Apify MCP server URL with a scoped tool parameter and your API token. This mapping translates the Actor's JSON input schema into a tool definition that the agent can call to generate PNG or PDF files. While some platform tools work unauthenticated, executing this specific Actor always requires a valid token in the request headers.

The connection string https://mcp.apify.com?tools=crawlerbros/screenshot-url tells the MCP server to only expose the schema for this specific generator. This is a critical optimization because it avoids hitting token limits with unrelated platform tools.

{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=crawlerbros/screenshot-url",
      "headers": {
        "Authorization": "Bearer YOUR_APIFY_TOKEN"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Once connected, the agent sees the input fields as function arguments. For example, urls is recognized as a required array of objects, and viewportWidth is seen as an integer with a default of 1280. The agent can then decide to trigger a capture when it needs visual verification of a UI layout or a record of a page's current state.

Why do agent tool calls drop when using scroll to bottom?

Tool calls drop because many MCP clients attempt synchronous execution, which the Apify platform hard-caps at 300 seconds before returning an HTTP 408 response. Enabling features like scrollToBottom or waitUntilNetworkIdleAfterScroll increases the processing time per URL, often pushing bulk requests past this 5 minute limit.

The screenshot-url Actor performs heavy browser work. If you provide a list of URLs and set scrollToBottom to true, the Actor must scroll in 250px increments and wait for lazy-loaded images. If the total time for all URLs exceeds 300 seconds, a standard synchronous tool call will fail with a timeout. Developers must implement a tool handler that manages the lifecycle of the run asynchronously rather than waiting for a single HTTP response.

// Example of an MCP-style tool handler for long-running screenshot tasks
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

const server = new McpServer({ name: "ApifyScreenshot", version: "1.0.0" });

server.tool("capture_screenshots", async (args) => {
    // Start the Actor run asynchronously to avoid the 300s sync cap
    const startResponse = await fetch(`https://api.apify.com/v2/acts/crawlerbros~screenshot-url/runs?token=${process.env.APIFY_TOKEN}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(args)
    });

    const runData = await startResponse.json();
    const run = runData.data;

    return {
        content: [{ 
            type: "text", 
            text: `Screenshot run started. ID: ${run.id}. Check results at the dataset URL.` 
        }]
    };
});
Enter fullscreen mode Exit fullscreen mode

Using this pattern ensures the agent does not hang while the browser engine navigates complex pages. The agent receives a run ID and can later query the dataset items once the status moves to SUCCEEDED.

What happens when vision models access expired screenshots?

Vision models encounter an HTTP 404 error if they attempt to fetch screenshot URLs from unnamed storage after the retention period ends. On the Apify free plan, unnamed datasets and key-value stores are only kept for 4 months, and the platform only retains the 10 most recent runs.

When screenshot-url finishes, it provides a screenshotUrl that points to a record in a key-value store. This URL is public but tied to the specific run. If your agentic workflow relies on these images for long-term historical analysis, you must move the files to a named storage object, which is exempt from automatic deletion.

# How to save a transient screenshot to a permanent named store for agent access
# 1. Fetch the screenshot bytes from the run's key-value store
curl -o temp_shot.png "https://api.apify.com/v2/key-value-stores/STORE_ID/records/screenshot_1.png"

# 2. Upload to a named store that won't expire
curl -X PUT "https://api.apify.com/v2/key-value-stores/permanent-visual-logs/records/site-audit.png?token=YOUR_TOKEN" \
  -H "Content-Type: image/png" \
  --data-binary "@temp_shot.png"
Enter fullscreen mode Exit fullscreen mode

Without this step, an agent looking back at visual logs from five runs ago might find the images have been purged to make room for newer executions.

How does screenshot-url handle single page application rendering?

The Actor handles Single Page Applications (SPAs) by using the waitUntil condition combined with a custom delay to allow the JavaScript framework to finish hydration. Setting the waitUntil parameter to networkidle is specifically designed for pages with heavy JavaScript rendering where the initial load event occurs before content is visible.

The input schema provides waitUntilNetworkIdleAfterScroll for exactly this reason. When scrolling to the bottom of a social media feed or a search results page, new content is fetched dynamically. Setting this to true tells the Actor to wait until no new network requests have been made for 500ms, ensuring the vision model sees the fully rendered content.

{
    "urls": [{"url": "https://example-app.com/dashboard"}],
    "format": "png",
    "waitUntil": "networkidle",
    "delay": 3000,
    "viewportWidth": 1440,
    "scrollToBottom": true,
    "waitUntilNetworkIdleAfterScroll": true
}
Enter fullscreen mode Exit fullscreen mode

This configuration ensures the browser stays open long enough for all AJAX calls to settle. The delay field provides a buffer for entrance animations that might otherwise result in a blurred or partial screenshot.

Why do API calls ignore the input schema prefill metadata?

API calls and MCP tool invocations ignore the prefill field because it is a UI-only attribute designed for the Apify Console web interface. If a developer assumes that a field will be populated based on the prefill value seen in the Actor documentation, their programmatic runs will receive undefined for that field.

Only the default value in the schema is automatically applied during an API call. For the screenshot-url Actor, this means format defaults to "png" and viewportWidth defaults to 1280. If you need the agent to use a specific CSS selector to hide cookie banners consistently, you cannot rely on the console's prefilled examples; you must pass them explicitly in the tool call body.

import requests

# Correct way: Explicitly passing all parameters to the API
# Do not rely on "prefill" values shown in the Apify Console
run_input = {
    "urls": ["https://www.wikipedia.org"],
    "format": "pdf",
    "selectorsToHide": ".cookie-banner, #popup",
    "scrollToBottom": True
}

response = requests.post(
    "https://api.apify.com/v2/acts/crawlerbros~screenshot-url/runs?token=YOUR_TOKEN",
    json=run_input
)
Enter fullscreen mode Exit fullscreen mode

By ensuring the agent explicitly provides values for selectorsToHide, you avoid the common issue where overlays like GDPR banners block the content of the screenshot.

Which input fields determine the total cost of a run?

The cost of a run is determined by the number of URL result events emitted and the memory allocated to the container, as this Actor uses a PAY_PER_EVENT pricing model. Every charge this Actor makes is one of the named events below, at the USD price shown. This list is exhaustive: there is no other cost and no separate platform-usage charge.

Charged events:

  • "Actor Start" (apify-actor-start): $0.01 per GB of memory allocated to the run.
  • "result" (apify-default-dataset-item): $0.002 per event (single result in the default dataset).

Volume-tier prices for the "result" event follow these exact Apify tiers:

  • FREE: $0.002
  • BRONZE: $0.00167
  • SILVER: $0.00133
  • GOLD: $0.001
  • PLATINUM: $0.001
  • DIAMOND: $0.001

The primary cost driver is the length of the urls array in your input. If an agent sends 500 URLs, it will generate 500 "result" events. Setting scrollToBottom or delay increases the run's duration, but since there is no compute time charge, these settings do not increase the price of the run. Only the number of URLs and the allocated memory matter for billing.

How to handle vision failures in agent tool logic?

Agents must be programmed to handle success statuses that contain internal rendering errors, as the Actor run itself will report a successful status even if a specific URL failed to load. The screenshot-url Actor records the outcome for each URL in the status field of the dataset item.

If a URL returns a 404 or a DNS error, the dataset item will exist, but the status will contain an error description like "net::ERR_NAME_NOT_RESOLVED". An agent that only checks if the run finished will mistakenly believe it has a valid screenshot. The logic must fetch the dataset items and verify the status field before attempting to pass a screenshotUrl to a vision model.

def validate_screenshot_results(dataset_items):
    valid_images = []
    for item in dataset_items:
        # The run succeeded, but did the individual screenshot succeed?
        if item.get("status") == "success":
            valid_images.append(item["screenshotUrl"])
        else:
            print(f"Skipping failed capture: {item['status']}")
    return valid_images

# Example dataset record shape based on output schema
sample_output = {
    "startUrl": "https://www.example.com",
    "url": "https://www.example.com/",
    "screenshotUrl": "https://api.apify.com/v2/key-value-stores/STORE_ID/records/file.png",
    "screenshotKey": "file.png",
    "format": "png",
    "status": "success",
    "timestamp": "2026-03-23T12:00:00.000Z"
}
Enter fullscreen mode Exit fullscreen mode

Furthermore, if the platform spend limit is reached via the maxTotalChargeUsd parameter (exposed as ACTOR_MAX_TOTAL_CHARGE_USD), the run will terminate. This termination is not instant; the container has a 30 second graceful abort window. Agents should check if a run was aborted to determine if they need to resurrect the run and restart the timeout clock.

What are the technical limitations of website screenshot generator?

The Actor is restricted to publicly accessible websites and cannot bypass authentication walls, handle CAPTCHAs, or manipulate fixed viewport height rules. While it is highly effective for public UI audits, it cannot be used for screens behind a login or for testing how elements behave at varying viewport heights, as the height is locked at 1080px.

Specific constraints to consider:

  1. No Session Handling: There is no input field for cookies or bearer tokens. The Actor navigates as a clean, unauthenticated browser.
  2. Viewport Height: Only viewportWidth is configurable (100px to 3840px). The height is always 1080px, though the "full-page" feature captures the entire document length.
  3. PDF Layout Differences: When using format as "pdf", the browser uses print media queries. This may hide elements like navigation bars or change font sizes compared to the visual PNG output.
  4. Proxy Session Limits: If you enable proxyConfiguration, be aware that datacenter proxy sessions persist for 26 hours, while residential sessions last about 30 minutes.
  5. Request Queue Isolation: A request queue can only be processed by one Actor run at a time. You cannot fan-out a single screenshot task across multiple simultaneous runs of this Actor if they share the same queue.

By understanding these boundaries, developers can build more resilient agent integrations. For example, if a target site blocks datacenter traffic, the agent must be instructed to provide a proxy configuration rather than simply retrying the same failing request.

How can automated schedules maintain screenshot archives?

Schedules allow you to run the screenshot tool at regular intervals using a 6-field cron syntax, but they require a successful prior run and are created in a disabled state. This means you cannot programmatically create a schedule and expect it to start immediately without an extra step to enable it.

The schedule's cron expression allows for 10 second intervals, but most screenshot tasks use daily or hourly triggers. Because unnamed storage expires, a schedule that runs every day will eventually lose its older screenshots unless the action in the schedule is configured to save results to a named key-value store.

{
    "name": "daily-ui-audit",
    "cronExpression": "0 0 0 * * *",
    "isEnabled": true,
    "actions": [
        {
            "type": "RUN_ACTOR",
            "actorId": "crawlerbros/screenshot-url",
            "input": {
                "urls": [{"url": "https://apify.com"}],
                "format": "png",
                "waitUntil": "networkidle"
            }
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

When integrating with external tools like n8n, you can avoid manual polling for these scheduled runs. The n8n platform has a native Apify Trigger node that responds to run completion events. This is more efficient than polling the API, as the webhook fires exactly when the screenshot is ready in the key-value store.

How to process large screenshot datasets with Node.js?

Processing datasets with hundreds of screenshots requires managing the platform's storage rate limits, which are capped at 60 requests per second for storage objects and 400 requests per second for dataset item pushes. If an agent tries to download many screenshots simultaneously after a bulk run, it may hit rate limits on the key-value store.

The following Node.js example demonstrates how to safely retrieve results and handle the asynchronous nature of the Actor to stay within the 300 second window.

const APIFY_TOKEN = process.env.APIFY_TOKEN;

async function getScreenshots(urlList) {
    // Start the run
    const response = await fetch('https://api.apify.com/v2/acts/crawlerbros~screenshot-url/runs?token=' + APIFY_TOKEN, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ urls: urlList, format: 'png' })
    });

    const runResult = await response.json();
    const runId = runResult.data.id;

    // Poll for completion to avoid 408 timeout on sync endpoint
    while (true) {
        const statusCheck = await fetch(`https://api.apify.com/v2/actor-runs/${runId}?token=${APIFY_TOKEN}`);
        const statusData = await statusCheck.json();
        const status = statusData.data.status;

        if (status === 'SUCCEEDED') {
            const datasetId = statusData.data.defaultDatasetId;
            const items = await fetch(`https://api.apify.com/v2/datasets/${datasetId}/items?token=${APIFY_TOKEN}`);
            return await items.json();
        } else if (['FAILED', 'ABORTED', 'TIMED-OUT'].includes(status)) {
            throw new Error(`Screenshot job failed: ${status}`);
        }

        // Wait 5 seconds between checks
        await new Promise(r => setTimeout(r, 5000));
    }
}
Enter fullscreen mode Exit fullscreen mode

This pattern is the standard for production-grade agent tools. It provides the agent with a robust way to wait for heavy rendering tasks that would otherwise trigger a timeout and leave the agent without the visual data it needs for its vision model analysis. Implementing these checks prevents the "silent failure" state where an agent assumes a tool succeeded but cannot find the resulting file.

Does the Actor support custom CSS for hiding elements?

Yes, the selectorsToHide field allows you to provide a comma-separated list of CSS selectors that the Actor will hide before capturing the image. This is essential for removing dynamic elements like cookie consent banners, popups, and advertisements that frequently obscure the main content of a screenshot.

For example, if you are capturing a series of blog posts, you might set the field to .cookie-banner, #newsletter-signup, .sidebar-ads. The Actor injects styles to set these elements to display: none before the browser captures the viewport. This ensures the vision model receives a clean representation of the target content without the distraction of marketing overlays.

When should you use PDF format instead of PNG?

You should use the PDF format when the target page is extremely long or when the final output is intended for printing or document archiving. While PNG captures a full-page image, very long pages can result in massive file sizes that are difficult to load into vision models or standard image viewers.

The format parameter in the input schema can be set to "pdf" to trigger the browser's printable document generator. This is particularly useful for text-heavy sites or academic papers where pagination is desirable. However, keep in mind that PDF generation uses the print stylesheet of the website, which may differ from the screen layout. If visual fidelity to what a user sees on a monitor is the priority, PNG remains the better choice for the generator.

The Actor's README is the source of truth for its inputs, outputs and limits. Need a hand wiring this into your stack? Email info@crawlerbros.com

Top comments (0)