Strands Agents + Anchor: Browser Agents in Python with AWS's Open-Source SDK

Hands On
Jun 25
by Idan Raman

Amazon open-sourced Strands Agents in May 2025 and it has quickly become the go-to framework for teams running on AWS who want a minimal, production-grade way to build autonomous agents. The programming model is intentionally lean: decorate a Python function with @tool, pass it to an Agent, and you're done. No DAGs, no boilerplate, no framework lock-in.

The missing piece is a browser. When your agent needs to log in to a SaaS dashboard, scrape a page behind a login wall, or click through a multi-step form, you need real browser infrastructure. That's where Anchor comes in.

How they fit together

Anchor provides cloud-hosted browser sessions with persistent identity—stable cookies, a real browser fingerprint, and IPs that pass Cloudflare and other bot filters. You expose that browser as a @tool, and Strands handles the reasoning loop that decides when and how to call it. The result is a production agent that can act on the real web, not just read from APIs.

Setup

pip install strands-agents playwright requests
playwright install chromium

export ANCHOR_API_KEY="your-anchor-api-key"
export ANTHROPIC_API_KEY="your-anthropic-api-key"

Strands Agents defaults to Claude via the Anthropic SDK but also supports Amazon Bedrock, OpenAI, and LiteLLM — swap the provider without changing any tool code.

Defining the browser tools

# agent.py
import os, requests
from playwright.sync_api import sync_playwright
from strands import Agent, tool

ANCHOR_KEY = os.environ["ANCHOR_API_KEY"]

def _new_session(profile_id: str | None = None) -> dict:
    payload = {"os_type": "linux", "screen": {"width": 1280, "height": 800}}
    if profile_id:
        payload["profile_id"] = profile_id
    resp = requests.post(
        "https://api.anchorbrowser.io/v1/sessions",
        headers={"anchor-api-key": ANCHOR_KEY, "Content-Type": "application/json"},
        json=payload,
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()["data"]

@tool
def navigate_and_extract(url: str, task: str) -> str:
    '''
    Open a browser, navigate to a URL, and return the visible page text.

    Args:
        url: The full URL to visit.
        task: Human-readable description of what to extract from the page.

    Returns:
        Visible text content of the page, truncated to 8 000 characters.
    '''
    session = _new_session()
    try:
        with sync_playwright() as p:
            browser = p.chromium.connect_over_cdp(session["cdp_url"])
            page = browser.new_page()
            page.goto(url, wait_until="domcontentloaded", timeout=60_000)
            content = page.evaluate("() => document.body.innerText")
            browser.close()
        return content[:8000]
    finally:
        requests.delete(
            f"https://api.anchorbrowser.io/v1/sessions/{session['id']}",
            headers={"anchor-api-key": ANCHOR_KEY},
        )

@tool
def fill_form(url: str, selectors_to_values: dict) -> str:
    '''
    Navigate to a page and fill a form using CSS selectors.

    Args:
        url: The page containing the form.
        selectors_to_values: Mapping of CSS selectors to the values to type.

    Returns:
        Confirmation string with the final URL after submission.
    '''
    session = _new_session()
    try:
        with sync_playwright() as p:
            browser = p.chromium.connect_over_cdp(session["cdp_url"])
            page = browser.new_page()
            page.goto(url, wait_until="networkidle", timeout=60_000)
            for selector, value in selectors_to_values.items():
                page.fill(selector, value)
            page.keyboard.press("Enter")
            page.wait_for_load_state("networkidle")
            final_url = page.url
            browser.close()
        return f"Form submitted. Final URL: {final_url}"
    finally:
        requests.delete(
            f"https://api.anchorbrowser.io/v1/sessions/{session['id']}",
            headers={"anchor-api-key": ANCHOR_KEY},
        )

Running the agent

# run.py
from agent import navigate_and_extract, fill_form
from strands import Agent

agent = Agent(tools=[navigate_and_extract, fill_form])

result = agent(
    "Go to https://news.ycombinator.com and give me a "
    "one-paragraph summary of the top 5 stories."
)
print(result)

Strands handles the full reasoning loop automatically. The agent decides which tool to call, passes the right arguments, reads the result, and iterates until it has a final answer. You write zero loop logic.

Persistent profiles for authenticated sessions

If your task requires a login—and most production workflows do—pass a profile_id when creating the session. The first run logs in and saves browser state under that ID. Every subsequent run rehydrates cookies and local storage so your agent stays authenticated without re-entering credentials.

# Authenticate once, reuse the profile on every future run
PROFILE_ID = "my-saas-dashboard"

agent = Agent(tools=[
    lambda url, task: navigate_and_extract.__wrapped__(
        url, task, profile_id=PROFILE_ID
    )
])

Pair this with Anchor's OmniConnect for zero-touch OAuth flows — the agent can complete a full OAuth handoff without you hard-coding credentials.

Parallel browser sessions

Strands Agents supports async tool execution. If your agent reasons about multiple pages in the same step — say, comparing prices across three e-commerce sites — each tool call gets its own Anchor session running concurrently. Anchor supports up to 5 000 simultaneous sessions, so the bottleneck will be your LLM context budget, not the infrastructure.

import asyncio
from strands import Agent

async def main():
    agent = Agent(tools=[navigate_and_extract])
    stream = agent.stream_async(
        "Compare the pricing pages of Vercel, Netlify, and Render. "
        "Return a markdown table with plan names and monthly costs."
    )
    async for event in stream:
        if "data" in event:
            print(event["data"], end="", flush=True)

asyncio.run(main())

Production tips

  • Trim tool output — Return only the relevant slice of page content (e.g., the section containing pricing tables) to keep token costs down. The [:8000] cap above is a safe starting point.
  • Clean up sessions — Always close sessions in a finally block. Leaked sessions count against your concurrent limit.
  • Geo-route with Anchor VPN — Add "vpn": {"type": "datacenter", "country_code": "DE"} to the session payload to route through a specific region for geo-sensitive tasks like price monitoring or compliance checks.
  • Retry on transient failures — Browser pages can time out under load. Wrap _new_session() in a short exponential-backoff retry loop before surfacing errors to the agent.

What's next

Strands Agents + Anchor takes about 15 minutes to get running from scratch. Once you have the two browser tools above, you can extend your agent in any direction — LinkedIn research, multi-step checkout flows, scheduled monitoring jobs — without touching the reasoning layer.

Try Anchor free →

Stay ahead in browser automation

We respect your inbox. Privacy policy

Welcome aboard! Thanks for signing up
Oops! Something went wrong while submitting the form.