DEV Community

Praise James for ZenRows

Posted on Originally published at zenrows.com

Integrating Zenrows into smolagents for production web access

This article was originally published on the Zenrows blog. Read the original here: https://www.zenrows.com/blog/zenrows-smolagents

This guide shows you how to replace smolagents' VisitWebpageTool with a Zenrows @tool that returns full Markdown from JavaScript-rendered and protected pages. You need Python 3.10+, a Zenrows API key, and a Hugging Face token.

All code is in the GitHub repository.

Before you start

Why replace VisitWebpageTool

VisitWebpageTool fetches with Python's requests library. On protected pages, the TLS handshake exposes the client signature and the target returns a challenge page instead of content. The agent receives that and reasons over it without knowing it's bad input.

Running VisitWebpageTool against a protected Walmart product page returned this:

Robot or human?
===============

Activate and hold the button to confirm that you're human. Thank You
Enter fullscreen mode Exit fullscreen mode

That response goes straight into the model. Research tasks, summarization, and classification all run on it and still produce output.

Step 1: Install packages

pip install smolagents requests python-dotenv
Enter fullscreen mode Exit fullscreen mode

Step 2: Add your API key

# .env
ZENROWS_API_KEY=your_zenrows_api_key_here
HF_TOKEN=your_hugging_face_token_here
Enter fullscreen mode Exit fullscreen mode

Step 3: Create the Zenrows fetch tool

Create zenrows_tool.py. The @tool decorator registers fetch_page with your CodeAgent. mode=auto lets Zenrows pick the retrieval strategy per target — you don't configure browser rendering or proxies manually.

import os

import requests
from dotenv import load_dotenv
from smolagents import tool

load_dotenv()

ZENROWS_API_KEY = os.getenv("ZENROWS_API_KEY")

@tool
def fetch_page(url: str) -> str:
    """
    Fetches webpage content and returns it as clean Markdown, including
    JavaScript-rendered and protected pages.

    Use this tool whenever you need to read a specific URL and retrieve
    webpage content for research, summarization, or analysis.

    Args:
        url: The webpage URL to fetch.
    """
    try:
        response = requests.get(
            "https://api.zenrows.com/v1/",
            params={
                "url": url,
                "apikey": ZENROWS_API_KEY,
                "mode": "auto",
                "response_type": "markdown",
            },
            timeout=30,
        )
        response.raise_for_status()
        return response.text

    except requests.RequestException as exc:
        raise RuntimeError(f"Failed to retrieve content from {url}") from exc
Enter fullscreen mode Exit fullscreen mode

Step 4: Test the fetch tool before wiring it to an agent

# add to the bottom of zenrows_tool.py
if __name__ == "__main__":
    result = fetch_page("https://www.walmart.com/ip/AirPods-Pro-3/17835006350")
    print(result[1500:2500])
Enter fullscreen mode Exit fullscreen mode
python zenrows_tool.py
Enter fullscreen mode Exit fullscreen mode

You should see Markdown from the product page instead of a bot check.

Write a docstring the model will actually use

smolagents builds the tool description from the function signature and docstring at runtime. A vague docstring means the agent skips the tool or writes its own fetch code.

Vague:

@tool
def fetch_page(url: str) -> str:
    """
    Fetches webpage content.

    Args:
        url: The webpage URL.
    """
Enter fullscreen mode Exit fullscreen mode

Specific (what's in zenrows_tool.py):

@tool
def fetch_page(url: str) -> str:
    """
    Fetches webpage content and returns it as clean Markdown, including
    JavaScript-rendered and protected pages.

    Use this tool whenever you need to read a specific URL and retrieve
    webpage content for research, summarization, or analysis.

    Args:
        url: The webpage URL to fetch.
    """
Enter fullscreen mode Exit fullscreen mode

The specific version tells the model what the tool returns, when to call it, and what page types it handles. That's what routes retrieval tasks to fetch_page instead of fallback behavior.

Step 5: Initialize the CodeAgent

Create agent.py and register fetch_page:

import os

from dotenv import load_dotenv
from smolagents import CodeAgent, InferenceClientModel

from zenrows_tool import fetch_page

load_dotenv()

model = InferenceClientModel(
    model_id="Qwen/Qwen2.5-7B-Instruct",
    token=os.getenv("HF_TOKEN"),
)

agent = CodeAgent(
    tools=[fetch_page],
    model=model,
)

response = agent.run(
    """
    Go to https://techcrunch.com/2026/07/23/amd-takes-on-nvidia-with-its-helios-ai-rack-scale-system/

    Read the article and identify:
    - the company involved,
    - the main announcement,
    - the news category,
    - why it matters.
    """,
    max_steps=8,
)

print(response)
Enter fullscreen mode Exit fullscreen mode

Step 6: Run the agent

python agent.py
Enter fullscreen mode Exit fullscreen mode

The run finished in three steps. The agent called fetch_page once and structured its output from the returned content:

{'company': 'AMD',
 'main_announcement': 'AMD has launched the Helios AI rack-scale system, challenging Nvidia in the AI rack-scale system market.',
 'news_category': 'AI',
 'why_it_matters': 'AMD is challenging Nvidia in the AI rack-scale system market, which is a significant move as Nvidia has historically dominated this market.'}
Enter fullscreen mode Exit fullscreen mode

Handling concurrent Hugging Face Spaces

Shared Spaces route all outbound requests through a shared IP pool. Sites enforcing IP-based rate limits see it as one source and respond with blocks or challenges.

fetch_page routes through Zenrows instead of directly from your Space to the target. mode=auto is already in the params block — no additional proxy config needed:

params={
    "url": url,
    "apikey": ZENROWS_API_KEY,
    "mode": "auto",         # Zenrows picks the retrieval strategy per target
    "response_type": "markdown",
}
Enter fullscreen mode Exit fullscreen mode

What's next

The same @tool pattern applies across frameworks:

Full project: GitHub repository

Top comments (0)