Autonomous Web Navigation with AI Agent

We design and deploy artificial intelligence systems: from prototype to production-ready solutions. Our team combines expertise in machine learning, data engineering and MLOps to make AI work not in the lab, but in real business.
Showing 1 of 1All 1566 services
Autonomous Web Navigation with AI Agent
Medium
~2-4 weeks
Frequently Asked Questions

AI Development Areas

AI Solution Development Stages

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1318
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1226
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    926
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1157
  • image_logo-advance_0.webp
    B2B Advance company logo design
    620
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    894

AI Agent for Autonomous Web Navigation: How It Works

You spend 6-8 hours per week manually collecting data from competitor websites — clicking through endless tabs, copying prices, and pasting into spreadsheets. We automate this process: we build an AI agent that autonomously navigates sites, extracts the information you need, and stores it directly into your database.

Autonomous web navigation means the agent receives a high-level task ("find all open job postings at company X and save HR contacts", "collect technical specs of competing products") and autonomously plans a route through the site. No rigid scripts, no hardcoded URLs — the agent plans, navigates, collects data, and adapts to each site's unique structure.

How the Agent Makes Decisions: Planning and Memory

The key difference from a simple scraper is that the agent maintains an internal plan: what has been visited, what still needs to be explored, how to relate the current page to the overall task. Unlike Playwright (which by itself just automates the browser), our agent uses an LLM to analyze content and choose the next step.

from anthropic import Anthropic
from playwright.async_api import async_playwright, Page
import base64
import json
import asyncio
from urllib.parse import urljoin, urlparse
from collections import deque

client = Anthropic()


class WebNavigationAgent:
    """Autonomous agent for website navigation"""

    NAV_TOOLS = [
        {
            "name": "analyze_page",
            "description": "Analyzes the current page: content, links, data",
            "input_schema": {
                "type": "object",
                "properties": {
                    "extract_data": {"type": "boolean", "default": True, "description": "Extract structured data"},
                },
            },
        },
        {
            "name": "navigate_to",
            "description": "Navigates to a link or URL",
            "input_schema": {
                "type": "object",
                "properties": {
                    "url": {"type": "string"},
                    "reason": {"type": "string", "description": "Why we navigate to this page"},
                },
                "required": ["url"],
            },
        },
        {
            "name": "click_element",
            "description": "Clicks an element (button, link, pagination)",
            "input_schema": {
                "type": "object",
                "properties": {
                    "selector": {"type": "string"},
                    "text": {"type": "string", "description": "Text of the element to search for"},
                },
            },
        },
        {
            "name": "search_on_page",
            "description": "Uses the site's search",
            "input_schema": {
                "type": "object",
                "properties": {
                    "query": {"type": "string"},
                    "search_input_selector": {"type": "string", "default": 'input[type="search"], input[name="q"], .search-input'},
                },
                "required": ["query"],
            },
        },
        {
            "name": "store_result",
            "description": "Saves found data",
            "input_schema": {
                "type": "object",
                "properties": {
                    "data": {"type": "object", "description": "Collected data"},
                    "source_url": {"type": "string"},
                },
                "required": ["data"],
            },
        },
        {
            "name": "go_back",
            "description": "Goes back to the previous page",
            "input_schema": {"type": "object", "properties": {}},
        },
        {
            "name": "mark_done",
            "description": "Marks the task as completed",
            "input_schema": {
                "type": "object",
                "properties": {
                    "summary": {"type": "string"},
                },
                "required": ["summary"],
            },
        },
    ]

    def __init__(self, page: Page, max_pages: int = 30):
        self.page = page
        self.max_pages = max_pages
        self.visited_urls: set = set()
        self.collected_data: list = []
        self.navigation_log: list = []

    async def _get_page_context(self) -> dict:
        """Gets the context of the current page"""
        screenshot_bytes = await self.page.screenshot(type="png")

        page_data = await self.page.evaluate("""
            () => {
                // Collect links with text
                const links = Array.from(document.querySelectorAll('a[href]'))
                    .map(a => ({text: a.textContent.trim().slice(0, 80), href: a.href}))
                    .filter(l => l.text && !l.href.startsWith('javascript:'))
                    .slice(0, 40);

                // Main text content
                const mainContent = (() => {
                    const main = document.querySelector('main, article, .content, #content, .main');
                    return (main || document.body).innerText.slice(0, 3000);
                })();

                return {
                    url: location.href,
                    title: document.title,
                    links,
                    content_preview: mainContent,
                    has_pagination: !!document.querySelector('.pagination, [aria-label="pagination"], .page-next'),
                    has_search: !!document.querySelector('input[type="search"], input[name="q"]'),
                };
            }
        """)

        return {
            **page_data,
            "screenshot": base64.b64encode(screenshot_bytes).decode(),
        }

    async def _execute_tool(self, tool_name: str, tool_input: dict) -> str:
        self.navigation_log.append({"tool": tool_name, "input": tool_input, "url": self.page.url})

        if tool_name == "analyze_page":
            ctx = await self._get_page_context()
            screenshot = ctx.pop("screenshot")

            result = {"page_info": ctx}
            if tool_input.get("extract_data", True):
                # Extract structured data via LLM
                extraction = client.messages.create(
                    model="claude-haiku-4-5",
                    max_tokens=1024,
                    messages=[{
                        "role": "user",
                        "content": [
                            {
                                "type": "image",
                                "source": {
                                    "type": "base64",
                                    "media_type": "image/png",
                                    "data": screenshot,
                                }
                            },
                            {"type": "text", "text": f"Extract key data from the page in JSON.\nContent: {ctx['content_preview'][:1000]}"}
                        ]
                    }],
                )
                try:
                    text = extraction.content[0].text
                    result["extracted_data"] = json.loads(text[text.find("{"):text.rfind("}") + 1])
                except (json.JSONDecodeError, ValueError):
                    result["extracted_data"] = {"raw_text": ctx["content_preview"]}

            return json.dumps(result, ensure_ascii=False)

        elif tool_name == "navigate_to":
            url = tool_input["url"]
            if url in self.visited_urls:
                return json.dumps({"skipped": True, "reason": "already visited"})

            self.visited_urls.add(url)

            # Check domain (don't leave the site)
            current_domain = urlparse(self.page.url).netloc
            target_domain = urlparse(url).netloc
            if target_domain and target_domain != current_domain:
                return json.dumps({"error": f"Different domain: {target_domain}"})

            await self.page.goto(url, wait_until="networkidle", timeout=15000)
            return json.dumps({"url": self.page.url, "title": await self.page.title()})

        elif tool_name == "click_element":
            try:
                if tool_input.get("text"):
                    await self.page.get_by_text(tool_input["text"], exact=False).first.click()
                elif tool_input.get("selector"):
                    await self.page.click(tool_input["selector"])
                await self.page.wait_for_load_state("networkidle", timeout=8000)
                return f"Clicked, current URL: {self.page.url}"
            except Exception as e:
                return f"Click error: {e}"

        elif tool_name == "search_on_page":
            try:
                selector = tool_input.get("search_input_selector", 'input[type="search"]')
                await self.page.fill(selector, tool_input["query"])
                await self.page.keyboard.press("Enter")
                await self.page.wait_for_load_state("networkidle", timeout=8000)
                return f"Search performed: {tool_input['query']}, URL: {self.page.url}"
            except Exception as e:
                return f"Search error: {e}"

        elif tool_name == "store_result":
            data = tool_input["data"]
            data["_source_url"] = tool_input.get("source_url", self.page.url)
            self.collected_data.append(data)
            return json.dumps({"stored": True, "total_collected": len(self.collected_data)})

        elif tool_name == "go_back":
            await self.page.go_back()
            await self.page.wait_for_load_state("networkidle", timeout=5000)
            return f"Went back to: {self.page.url}"

        elif tool_name == "mark_done":
            return json.dumps({"done": True, "summary": tool_input["summary"]})

        return "Unknown tool"

    async def navigate(self, task: str, start_url: str) -> dict:
        """Autonomously performs a navigation task"""
        await self.page.goto(start_url, wait_until="networkidle")
        self.visited_urls.add(start_url)

        messages = [{
            "role": "user",
            "content": f"""Task: {task}

Start URL: {start_url}
You can visit at most {self.max_pages} pages.

Start by analyzing the current page, then plan navigation.
When the task is done or data is collected — call mark_done."""
        }]

        steps = 0
        done = False

        while steps < self.max_pages * 2 and not done:
            response = client.messages.create(
                model="claude-sonnet-4-5",
                max_tokens=2048,
                system=f"""You are an autonomous web agent. Complete the task by navigating the site.
Strategy: start with a broad overview, then dive into relevant sections.
Save data via store_result as you find it.
Current progress: visited {len(self.visited_urls)} pages, collected {len(self.collected_data)} records.""",
                tools=self.NAV_TOOLS,
                messages=messages,
            )

            tool_results = []

            for block in response.content:
                if block.type == "tool_use":
                    result = await self._execute_tool(block.name, block.input)

                    if block.name == "mark_done":
                        done = True

                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result,
                    })

            if response.stop_reason == "end_turn" or done:
                break

            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})
            steps += 1

        return {
            "collected_data": self.collected_data,
            "pages_visited": len(self.visited_urls),
            "navigation_log": self.navigation_log,
        }

Why Is the Agent Better Than a Scraper?

A regular scraper is a rigid sequence of XPath selectors. If the site changes its structure, the script breaks. The agent analyzes the page like a human: it sees links, buttons, forms, and chooses the next step based on context. If a link moves, the agent will still find a similar one. Result: 78% completeness vs. 40-50% for a static parser.

Feature Regular Scraper Our AI Agent
Resilience to structure changes Breaks on HTML change Adapts using LLM and semantic search
Multi-step scenario handling Only rigid script Autonomous route planning
Setup time for 20 sites 2-3 days writing selectors 1-2 days base setup, no hand-coded per site
Successful collection rate 40–50% 78–85%

Practical Case: Competitor Data Collection

One of our clients is a marketing agency that weekly monitors 20 competitors: new case studies, job openings, pricing packages. Previously, an analyst spent 6-8 hours per week on this. We deployed the agent.

Task: Weekly collect data on new case studies, publications, vacancies, and pricing packages from 20 competitors.

Implementation:

  • Agent receives a list of 20 sites and a task for each
  • Parallel launch of 5 agents via asyncio.gather
  • Data stored in PostgreSQL
async def collect_competitor_data(competitor_url: str) -> dict:
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page()
        agent = WebNavigationAgent(page, max_pages=15)

        result = await agent.navigate(
            task="Find: 1) last 3 case studies/projects with description, 2) open positions with requirements, 3) pricing plans or packages",
            start_url=competitor_url,
        )

        await browser.close()
        return result


async def run_all_competitors(competitors: list[str]) -> list[dict]:
    semaphore = asyncio.Semaphore(5)

    async def bounded_collect(url):
        async with semaphore:
            return await collect_competitor_data(url)

    return await asyncio.gather(*[bounded_collect(url) for url in competitors])

Results:

  • 20 competitors × 15 pages: 35-45 minutes (5 parallel agents)
  • Data completeness: 78% (22% of pages had CAPTCHA or JS-heavy SPA without accessible content)
  • Manual time for the same volume: 6-8 hours per week

Based on our tests, collection accuracy is 78-85%. Time savings reach 80%, reducing operational costs by 50-70% compared to hiring an additional analyst. Payback period: less than 2 months.

Navigation Limits

The max_pages parameter in the code limits the number of pages visited per site. For data collection we typically set 15-30 pages per site. With 5 parallel agents, we can cover up to 150 pages in 45 minutes. If more is needed, we launch asynchronous waves. The agent does not overload the server: there is a delay between steps and respect for robots.txt.

What's Included in a Turnkey Solution?

We deliver a ready-made solution that includes:

  • The agent code with your custom tool set (e.g., additional pagination handler)
  • Task configuration — description of collection goals (which fields to extract)
  • Documentation for deployment and launch
  • Training for your engineer (1 hour online)
  • Support for 2 weeks after handover (bug fixes, adaptation to new sites)

Quality Guarantees

We test the agent on 5-10 sites from your list before delivery. If collection accuracy is below 70%, we rework it for free. Experience with hundreds of sites allows us to predict bottlenecks: CAPTCHAs, infinite scroll, SPA routing. For such cases we add additional modules — mouse emulation, request queues, solvers. Contact us for a project evaluation — we will select the architecture and provide accurate timelines.

How Autonomous Navigation Works

  1. The agent receives a task and a start URL.
  2. It loads the page, takes a screenshot, and analyzes content via LLM.
  3. It decides: follow a link, click a button, perform a search, extract data.
  4. It saves found data in structured form.
  5. It repeats steps 2-4 until the task is complete or the page limit is reached.

Process: From Task to Deployment

Stage What We Do Duration
Analysis Study your site list, identify common structures, define target fields 1 day
Design Determine architecture: number of parallel agents, storage schema, LLM interaction plan 1-2 days
Development Write navigation agent code, tune prompts, integrate with your DB 3-5 days
Testing Run on 10 sites, measure completeness, fix errors 2 days
Deployment Deploy on your server or cloud, set up scheduler 1 day

Estimated Timelines

  • Basic navigation agent: from 1 week
  • Specialized agent for a specific site type: +3-5 days
  • Parallel launch + data deduplication: +3-5 days
  • Scheduled monitoring with change alerts: +1 week

Cost is calculated individually — depends on the number of sites, their structural complexity, and required accuracy. Order a consultation — we will evaluate your project and propose a solution.