Agno (formerly PhiData) has quietly become one of the fastest-growing Python agent frameworks. Its design is refreshingly direct: agents are first-class objects with tools, memory, and structured output — and they compose into multi-agent teams without orchestration ceremony. It crossed 20k GitHub stars without a lot of fanfare, because developers who try it tend to keep using it.
Pair Agno with Anchor's managed cloud browsers — session isolation, residential proxies, CAPTCHA handling — and you get a browser agent stack that's ergonomic to build with and reliable to run in production.
Why Agno?
- Native multi-agent teams. Agno's
Teamclass coordinates multiple agents on a shared task. No custom orchestrator to write — just declare roles and let the framework handle handoffs. - Structured output by default. Pass any Pydantic model as
response_modeland the agent returns validated Python objects, not strings to parse. - Built-in storage. Sessions, run logs, and agent memories persist to SQLite or PostgreSQL with no extra configuration.
- Model-agnostic. OpenAI, Anthropic, Gemini, and local models all use the same
Agentinterface — swap in one line.
Setup
pip install agno anchorbrowser playwright
playwright install chromium
export ANCHOR_API_KEY="your_anchor_key"
export OPENAI_API_KEY="your_openai_key"
Defining Browser Tools
Agno tools are plain Python functions decorated with @tool. The framework inspects the docstring and type hints to build the LLM schema automatically:
import os
from anchorbrowser import AnchorClient
from playwright.sync_api import sync_playwright
from agno.tools import tool
anchor = AnchorClient(api_key=os.environ["ANCHOR_API_KEY"])
session = anchor.sessions.create()
pw = sync_playwright().start()
browser = pw.chromium.connect_over_cdp(session.data.cdpUrl)
page = browser.contexts[0].pages[0]
@tool
def navigate(url: str) -> str:
'Navigate to a URL and return the page title.'
page.goto(url, wait_until="domcontentloaded")
return f"At {url!r} — title: {page.title()}"
@tool
def get_text(selector: str = "body") -> str:
'Extract visible text from a CSS selector on the current page.'
return page.inner_text(selector)[:5000]
@tool
def click(selector: str) -> str:
'Click an element matching the CSS selector.'
page.click(selector)
page.wait_for_load_state("networkidle", timeout=5000)
return f"Clicked {selector!r}. Now at: {page.url}"
@tool
def type_text(selector: str, text: str) -> str:
'Fill an input field matching the CSS selector with text.'
page.fill(selector, text)
return f"Typed into {selector!r}"
Your First Agent
from agno.agent import Agent
from agno.models.openai import OpenAIChat
agent = Agent(
model=OpenAIChat(id="gpt-4o"),
tools=[navigate, get_text, click, type_text],
instructions=[
"You are a browser automation agent.",
"Use navigate first, then extract or interact with the page.",
],
show_tool_calls=True,
markdown=True,
)
agent.print_response(
"Go to news.ycombinator.com and list the top 5 story titles and scores.",
stream=True,
)
Structured Output
For production pipelines you want typed data. Define a Pydantic model and pass it as response_model — the agent validates its own output before returning:
from pydantic import BaseModel
from typing import List
class Story(BaseModel):
title: str
score: int
url: str
class HNResult(BaseModel):
stories: List[Story]
structured_agent = Agent(
model=OpenAIChat(id="gpt-4o"),
tools=[navigate, get_text],
response_model=HNResult,
)
result: HNResult = structured_agent.run(
"Go to news.ycombinator.com and extract the top 10 stories."
)
for story in result.stories:
print(f"[{story.score:>4}] {story.title}")
Multi-Agent Teams
Agno's Team class assigns specialized roles and coordinates handoffs. Here a navigator explores the page while an analyst extracts structured insights:
from agno.team import Team
navigator = Agent(
name="Navigator",
model=OpenAIChat(id="gpt-4o-mini"),
tools=[navigate, click],
instructions=["Navigate to the requested URL and describe what you see."],
)
analyst = Agent(
name="Analyst",
model=OpenAIChat(id="gpt-4o"),
tools=[get_text],
instructions=["Read the page content and return structured insights."],
response_model=HNResult,
)
team = Team(
members=[navigator, analyst],
model=OpenAIChat(id="gpt-4o"),
instructions=["Navigator goes first, then Analyst extracts data."],
show_tool_calls=True,
)
team.print_response("Analyze the top stories on news.ycombinator.com")
Persistent Storage
Agno persists session history to SQLite with no extra configuration. Pass a fixed session_id and the agent resumes where it left off:
from agno.storage.sqlite import SqliteStorage
storage = SqliteStorage(
table_name="browser_agent_runs",
db_file="runs.db",
)
persistent_agent = Agent(
model=OpenAIChat(id="gpt-4o"),
tools=[navigate, get_text],
storage=storage,
add_history_to_messages=True,
session_id="hn-monitor",
)
# First run seeds the session
persistent_agent.run("What are the top HN stories today?")
# Second run picks up context from the first
persistent_agent.run("Which of those have the most comments?")
Switching to Claude
The model swap is one line — tools and response models are unchanged:
from agno.models.anthropic import Claude
claude_agent = Agent(
model=Claude(id="claude-sonnet-4-6"),
tools=[navigate, get_text, click, type_text],
instructions=["You are a precise browser automation agent."],
response_model=HNResult,
)
Production Tips
- Scope sessions to runs. Create a fresh Anchor session per agent run and close it in a
finallyblock. This avoids session bleed across concurrent jobs. - Set
max_retries. Passmax_retries=3toAgent()for automatic retry on tool failures — handles transient page load issues without custom logic. - Use
debug_mode=Trueduring development. Agno logs every tool call and LLM response. Flip it off in production. - Parallel sessions. Agno has a native async API —
await agent.arun()— so you can fan out across multiple Anchor sessions withasyncio.gather(). See our parallel agents guide for patterns.
What's Next
Try Anchor free and run your first Agno browser agent in minutes →



