> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zenrows.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> The Zenrows API (Fetch, Extract via extract=auto, and Batch) is described in OpenAPI 3.1 at https://www.zenrows.com/openapi.json and listed in the RFC 9727 API catalog at https://www.zenrows.com/.well-known/api-catalog. Use the spec for exact parameter names and types.

# Fetch first, open a Browser when a task needs interaction

> Give your AI agent two web tools: fast page reading with Zenrows Fetch, and a full Zenrows Browser for logins, clicks and forms.

Give your agent two web tools and a simple rule: read with Fetch, interact with Browser. Fetch returns any page as markdown in one request. Browser runs a real cloud browser that your code drives step by step, for tasks like logging in, filling a form or clicking through a flow.

**Uses:** [Fetch](/fetch/api-reference) with [Adaptive Stealth Mode](/fetch/features/adaptive-stealth-mode) (`mode=auto`) and [`response_type=markdown`](/fetch/features/markdown), [Browser](/browser-sessions/introduction) with [Playwright](/browser-sessions/get-started/playwright)

## How it works

1. **The agent describes the task.** A read-only task is a URL. An interactive task is a URL plus a list of steps (`fill`, `click`, `wait_for_text`, `goto`).
2. **The router picks the primitive.** No steps: the task goes to Fetch with `mode=auto`, which renders JavaScript and switches to premium proxies only when the page needs them. Steps: the task goes to Browser.
3. **Fetch returns markdown.** One request, one page, ready for the model's context.
4. **Browser runs the steps.** Playwright connects to `wss://browser.zenrows.com` over CDP, runs each step in order, returns the page text, and closes the session.

Reading is the common case and Fetch is the fast, low-cost path for it. Browser is there for every task that needs a session with state.

## Ask your AI agent

With the [Zenrows MCP server](/mcp/overview) connected to Claude, Cursor or any MCP client, your agent already has both paths: fast reading and a full browser. Paste this prompt:

```text theme={"dark"}
Use Zenrows to read https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie/
and tell me its price. Then use the Zenrows browser to log in at https://www.scrapingcourse.com/login
with admin@example.com / password, and list the first three products on the page you land on.
Close the browser session when you're done.
```

## Build the router

Install the dependencies: `pip install requests playwright` for Python, or `npm install playwright-core` for Node.js. Browser runs in the cloud, so there is no local browser to download.

<CodeGroup>
  ```python Python theme={"dark"}
  import requests
  from playwright.sync_api import sync_playwright

  ZENROWS_API_KEY = "YOUR_ZENROWS_API_KEY"

  def read_page(url: str) -> str:
      """Fetch: read one page as markdown. Use for any task that only reads."""
      response = requests.get(
          "https://api.zenrows.com/v1/",
          params={
              "apikey": ZENROWS_API_KEY,
              "url": url,
              "mode": "auto",
              "response_type": "markdown",
          },
          timeout=180,
      )
      response.raise_for_status()
      return response.text

  def run_browser_task(url: str, steps: list[dict]) -> str:
      """Browser: open the page, run each step in order, return the final page text."""
      with sync_playwright() as p:
          browser = p.chromium.connect_over_cdp(
              f"wss://browser.zenrows.com?apikey={ZENROWS_API_KEY}"
          )
          try:
              page = browser.contexts[0].new_page()
              page.goto(url)
              for step in steps:
                  action, selector = step["action"], step.get("selector")
                  if action == "fill":
                      page.fill(selector, step["value"])
                  elif action == "click":
                      page.click(selector, no_wait_after=True)
                  elif action == "wait_for_text":
                      page.wait_for_function(
                          "text => document.body.innerText.includes(text)", arg=step["value"]
                      )
                  elif action == "goto":
                      page.goto(step["value"])
              return page.evaluate("document.body.innerText")
          finally:
              browser.close()

  def handle_web_task(url: str, steps: list[dict] | None = None) -> str:
      """Route: Fetch when the agent only reads, Browser when it must interact."""
      if not steps:
          return read_page(url)
      return run_browser_task(url, steps)

  if __name__ == "__main__":
      # Read-only task: goes to Fetch
      page = handle_web_task("https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie/")
      print("FETCH:", page[page.find("# Abominable"):][:120])

      # Multi-step task: goes to Browser
      result = handle_web_task(
          "https://www.scrapingcourse.com/login",
          steps=[
              {"action": "fill", "selector": "#email", "value": "admin@example.com"},
              {"action": "fill", "selector": "#password", "value": "password"},
              {"action": "click", "selector": "#submit-button"},
              {"action": "wait_for_text", "value": "Welcome"},
          ],
      )
      print("BROWSER:", result[:200])
  ```

  ```javascript Node.js theme={"dark"}
  import { chromium } from "playwright-core";

  const ZENROWS_API_KEY = "YOUR_ZENROWS_API_KEY";

  // Fetch: read one page as markdown. Use for any task that only reads.
  async function readPage(url) {
    const params = new URLSearchParams({
      apikey: ZENROWS_API_KEY,
      url,
      mode: "auto",
      response_type: "markdown",
    });
    const response = await fetch(`https://api.zenrows.com/v1/?${params}`, {
      signal: AbortSignal.timeout(180_000),
    });
    if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
    return response.text();
  }

  // Browser: open the page, run each step in order, return the final page text.
  async function runBrowserTask(url, steps) {
    const browser = await chromium.connectOverCDP(`wss://browser.zenrows.com?apikey=${ZENROWS_API_KEY}`);
    try {
      const page = await browser.contexts()[0].newPage();
      await page.goto(url);
      for (const step of steps) {
        if (step.action === "fill") await page.fill(step.selector, step.value);
        else if (step.action === "click") await page.click(step.selector, { noWaitAfter: true });
        else if (step.action === "wait_for_text")
          await page.waitForFunction((text) => document.body.innerText.includes(text), step.value);
        else if (step.action === "goto") await page.goto(step.value);
      }
      return await page.evaluate(() => document.body.innerText);
    } finally {
      await browser.close();
    }
  }

  // Route: Fetch when the agent only reads, Browser when it must interact.
  async function handleWebTask(url, steps = []) {
    return steps.length ? runBrowserTask(url, steps) : readPage(url);
  }

  const page = await handleWebTask("https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie/");
  console.log("FETCH:", page.slice(page.indexOf("# Abominable"), page.indexOf("# Abominable") + 60));

  const result = await handleWebTask("https://www.scrapingcourse.com/login", [
    { action: "fill", selector: "#email", value: "admin@example.com" },
    { action: "fill", selector: "#password", value: "password" },
    { action: "click", selector: "#submit-button" },
    { action: "wait_for_text", value: "Welcome" },
  ]);
  console.log("BROWSER:", result.slice(0, 120));
  ```
</CodeGroup>

The `finally` block closes the browser on every path, so no session stays open after the task. The login page uses the demo credentials shown on <a href="https://www.scrapingcourse.com/login" rel="nofollow">scrapingcourse.com/login</a>.

## Expose it as agent tools

Give the model the two functions as tools. The definitions use JSON Schema, which every agent framework accepts: pass `input_schema` as-is to the Anthropic API, or rename it to `parameters` for OpenAI function calling and most other frameworks.

```python Python theme={"dark"}
TOOLS = [
    {
        "name": "read_page",
        "description": "Read a web page and return its content as markdown. Use it for any task that only needs to read a page: articles, product pages, docs, search results.",
        "input_schema": {
            "type": "object",
            "properties": {"url": {"type": "string", "description": "Full URL of the page to read."}},
            "required": ["url"],
        },
    },
    {
        "name": "browser_task",
        "description": "Open a page in a cloud browser and run steps in order, then return the page text. Use it only when the task needs interaction: logging in, filling forms, clicking buttons.",
        "input_schema": {
            "type": "object",
            "properties": {
                "url": {"type": "string", "description": "Full URL of the page to open first."},
                "steps": {
                    "type": "array",
                    "description": "Actions to run in order.",
                    "items": {
                        "type": "object",
                        "properties": {
                            "action": {"type": "string", "enum": ["fill", "click", "wait_for_text", "goto"]},
                            "selector": {"type": "string", "description": "CSS selector, for fill and click."},
                            "value": {"type": "string", "description": "Text to type (fill), text to wait for (wait_for_text) or URL (goto)."},
                        },
                        "required": ["action"],
                    },
                },
            },
            "required": ["url", "steps"],
        },
    },
]

def run_tool(name: str, args: dict) -> str:
    """Call this with each tool call the model makes, and send the result back to it."""
    if name == "read_page":
        return read_page(args["url"])
    if name == "browser_task":
        return run_browser_task(args["url"], args["steps"])
    raise ValueError(f"Unknown tool: {name}")
```

The tool descriptions carry the routing rule, so the model picks `read_page` for reading and `browser_task` only when a step needs interaction. For a complete agent loop around these tools, see [Build a research agent with live web data](/academy/pipelines/research-agent).

## Example output

Output of the router, one Fetch task and one Browser task:

```text theme={"dark"}
FETCH: # Abominable Hoodie

$69.00

This is a variable product called a Abominable Hoodie

Size Choose an optionXSSMLXL Color C
BROWSER: Scraping
Course
Welcome, Scraper! Logout
Login Challenge
Challenge

Authentication Protected Data

Chaz Kangeroo Hoodie
$52
Teton Pullover Hoodie
$70
Bruno Compete Hoodie
$63
```

## Customize it

| Change | How |
| - | - |
| Read from a specific country | Add `proxy_country` to the Fetch params and to the Browser URL (`&proxy_country=es`), see [Proxy country](/fetch/features/proxy-country) and [Browser country](/browser-sessions/features/country) |
| Longer browser tasks | Add `&session_ttl=10m` to the Browser URL, see [Session TTL](/browser-sessions/features/session-ttl) |
| Light page actions without a browser session | Use Fetch with `js_instructions` (clicks, scrolls, waits), see [Interact with a page before scraping it](/academy/fetch/page-interactions) |
| Return structured data | Use `extract=auto` or `css_extractor` in `read_page`, see [Get structured JSON from any page](/academy/extract/auto-extraction) |
| More browser actions | Add branches for `select_option`, `press`, `scroll` or `screenshot`, see the <a href="https://playwright.dev/python/docs/api/class-page" rel="nofollow">Playwright API</a> |
| Use Puppeteer | Connect with `puppeteer.connect({ browserWSEndpoint })`, see [Puppeteer](/browser-sessions/get-started/puppeteer) |

## Troubleshooting

* **A click step times out waiting for navigation**: the click submits a form. Keep `no_wait_after=True` (`noWaitAfter: true` in Node.js) on clicks and follow them with a `wait_for_text` step for text on the next page.
* **`wait_for_text` times out**: the text never appeared, often because the previous step failed (wrong credentials, wrong selector). Return `page.evaluate("document.body.innerText")` in the error to see what the page shows.
* **The model uses the browser for simple reads**: tighten the `browser_task` description, for example "Only call this when a step needs a click, a form or a login."
* **Fetch returns `422`**: the page wasn't retrieved. Keep `mode=auto` and set the client timeout to 180 seconds so it has time to escalate, see [error codes](/api-error-codes).

## Related recipes

<CardGroup cols={2}>
  <Card title="Build a research agent with live web data" icon="robot" href="/academy/pipelines/research-agent">
    Wire Zenrows tools into an LLM agent loop.
  </Card>

  <Card title="Log in once and reuse the session" icon="login" href="/academy/browser/login-and-reuse-sessions">
    Save the logged-in state and reuse it across sessions.
  </Card>

  <Card title="Connect Playwright or Puppeteer to Zenrows Browser" icon="plug-connected" href="/academy/browser/connect-playwright-and-puppeteer">
    Every connection option for Browser.
  </Card>

  <Card title="Connect your AI agent to Zenrows" icon="plug" href="/academy/connect-your-ai-agent">
    Give Claude, Cursor or your own agent live web access.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.