By Sagar Shankaran, Founder of CallSphere
Build a complete screenshot-action loop where GPT-4V analyzes web pages, decides where to click, and navigates autonomously. Learn coordinate extraction, click targeting, and navigation decision-making.
Key takeaways
A vision-based web navigator follows a simple but powerful loop: capture a screenshot, send it to GPT-4V for analysis, extract the next action, execute that action in the browser, then repeat. This is the same observe-think-act cycle that underpins all agentic systems, applied to web browsing.
The key insight is that GPT-4V does not need access to the DOM. It looks at the rendered page and decides what a human would click next.
The navigator needs three components: a browser controller, a vision analyzer, and an action executor.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
flowchart LR
GOAL(["High level goal"])
PLAN["Planner LLM"]
SCREEN["Screen capture<br/>every step"]
VLM["Vision LLM<br/>reads UI state"]
ACT{"Action type"}
CLICK["Click coordinate"]
TYPE["Type text"]
KEY["Keyboard shortcut"]
GUARD["Safety filter<br/>allow lists"]
OS[("OS sandbox<br/>ephemeral VM")]
DONE(["Goal verified"])
GOAL --> PLAN --> SCREEN --> VLM --> ACT
ACT --> CLICK --> GUARD
ACT --> TYPE --> GUARD
ACT --> KEY --> GUARD
GUARD --> OS --> SCREEN
OS --> DONE
style PLAN fill:#4f46e5,stroke:#4338ca,color:#fff
style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
style OS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style DONE fill:#059669,stroke:#047857,color:#fff
import asyncio
import base64
from dataclasses import dataclass
from playwright.async_api import async_playwright, Page
from openai import OpenAI
@dataclass
class BrowserAction:
action_type: str # click, type, scroll, wait, done
x: int = 0
y: int = 0
text: str = ""
reasoning: str = ""
class VisionNavigator:
def __init__(self):
self.client = OpenAI()
self.history: list[str] = []
self.max_steps = 15
async def capture(self, page: Page) -> str:
"""Capture viewport screenshot as base64."""
screenshot = await page.screenshot(type="png")
return base64.b64encode(screenshot).decode("utf-8")
async def decide_action(
self, screenshot_b64: str, task: str
) -> BrowserAction:
"""Ask GPT-4V what action to take next."""
history_context = "\n".join(
f"Step {i+1}: {h}" for i, h in enumerate(self.history)
)
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": (
"You are a web navigation agent. Given a screenshot "
"and a task, decide the next action. The viewport is "
"1280x720 pixels. Respond in this exact format:\n"
"ACTION: click|type|scroll|done\n"
"X: <pixel x coordinate>\n"
"Y: <pixel y coordinate>\n"
"TEXT: <text to type, if action is type>\n"
"REASONING: <why this action>"
),
},
{
"role": "user",
"content": [
{
"type": "text",
"text": (
f"Task: {task}\n\n"
f"Previous actions:\n{history_context}\n\n"
"What should I do next?"
),
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{screenshot_b64}",
"detail": "high",
},
},
],
},
],
max_tokens=300,
)
return self._parse_action(response.choices[0].message.content)
def _parse_action(self, text: str) -> BrowserAction:
"""Parse the model's response into a BrowserAction."""
lines = text.strip().split("\n")
action = BrowserAction(action_type="done")
for line in lines:
if line.startswith("ACTION:"):
action.action_type = line.split(":", 1)[1].strip().lower()
elif line.startswith("X:"):
action.x = int(line.split(":", 1)[1].strip())
elif line.startswith("Y:"):
action.y = int(line.split(":", 1)[1].strip())
elif line.startswith("TEXT:"):
action.text = line.split(":", 1)[1].strip()
elif line.startswith("REASONING:"):
action.reasoning = line.split(":", 1)[1].strip()
return action
The action executor translates GPT-4V's decisions into Playwright commands.
async def execute_action(
self, page: Page, action: BrowserAction
) -> None:
"""Execute a browser action."""
if action.action_type == "click":
await page.mouse.click(action.x, action.y)
await page.wait_for_load_state("networkidle")
elif action.action_type == "type":
await page.mouse.click(action.x, action.y)
await page.keyboard.type(action.text, delay=50)
elif action.action_type == "scroll":
await page.mouse.wheel(0, action.y)
await asyncio.sleep(0.5)
async def run(self, url: str, task: str) -> list[str]:
"""Run the full navigation loop."""
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page(
viewport={"width": 1280, "height": 720}
)
await page.goto(url, wait_until="networkidle")
for step in range(self.max_steps):
screenshot = await self.capture(page)
action = await self.decide_action(screenshot, task)
self.history.append(
f"{action.action_type} at ({action.x},{action.y}) "
f"- {action.reasoning}"
)
if action.action_type == "done":
break
await self.execute_action(page, action)
await browser.close()
return self.history
GPT-4V's coordinate accuracy improves dramatically when you overlay a labeled grid on the screenshot. This gives the model reference points to anchor its position estimates.
from PIL import Image, ImageDraw, ImageFont
import io
def add_grid_overlay(
screenshot_bytes: bytes, grid_size: int = 100
) -> bytes:
"""Add a numbered grid overlay to a screenshot."""
img = Image.open(io.BytesIO(screenshot_bytes))
draw = ImageDraw.Draw(img, "RGBA")
width, height = img.size
marker_id = 0
for y in range(0, height, grid_size):
draw.line([(0, y), (width, y)], fill=(255, 0, 0, 80), width=1)
for x in range(0, width, grid_size):
if y == 0:
draw.line(
[(x, 0), (x, height)], fill=(255, 0, 0, 80), width=1
)
draw.text((x + 2, y + 2), str(marker_id), fill=(255, 0, 0, 180))
marker_id += 1
buffer = io.BytesIO()
img.save(buffer, format="PNG")
return buffer.getvalue()
With this overlay, you can instruct GPT-4V to report actions relative to grid markers: "click near marker 34" is far more reliable than "click in the middle-left area."
async def main():
navigator = VisionNavigator()
history = await navigator.run(
url="https://example.com",
task="Find the contact page and note the email address"
)
for entry in history:
print(entry)
asyncio.run(main())
Without a grid overlay, coordinates can be off by 30-80 pixels. With a labeled grid overlay at 100px intervals, accuracy improves to within 10-20 pixels. For small targets like radio buttons, use a click-then-verify pattern: click, take a new screenshot, and confirm the expected change occurred.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
Each screenshot at high detail consumes roughly 1000-1500 tokens. With conversation history, a practical limit is 15-25 steps before you approach context limits. For longer workflows, summarize earlier steps into text and drop old screenshots from the message history.
Each step takes 2-5 seconds: roughly 1 second for screenshot capture and 2-4 seconds for GPT-4V analysis. This is slower than DOM-based automation but acceptable for tasks where reliability matters more than speed, such as monitoring, testing, or data extraction from sites with unpredictable markup.
#VisionNavigator #GPT4V #BrowserAutomation #AgenticAI #WebNavigation #Playwright #ScreenshotLoop #Python

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
The 2026 desktop AI agent landscape — ServiceNow Project Arc, Anthropic Claude offerings, OpenAI agents, and Google Mariner. A buyer's map.
Build a working computer-use agent with the OpenAI Computer Use tool — clicks, types, scrolls a real browser — then evaluate task success on a benchmark suite.
Build a browser agent with LangGraph and Playwright that does multi-step web tasks, then ground-truth its work with visual diffs and DOM-based evaluators.
An agentic-AI perspective on Anthropic Skills system, covering orchestration patterns, tool use, and how agent tooling fits production agent stacks.
Enterprise CIO Guide perspective on Comet's general-availability launch put an agentic browser in front of millions of consumers, and it works better than the demos suggested.
Enterprise CIO Guide perspective on Harvey AI's enterprise rollout numbers show legal agents have moved past the pilot stage at AmLaw 100 firms.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco