Build a browser agent with Agno
Integrate Steel with the Agno agent framework.
Agno's primitive is Agent(model=..., tools=[...]). Tools are typed Python methods grouped into a Toolkit subclass; docstrings and type hints become the JSON schema the model sees. No manual function-calling boilerplate, no separate tool-registration step.
This recipe wraps a Steel browser in a SteelTools toolkit, hands it to an Agent, and lets the model drive the session by picking which method to call and filling in arguments on its own.
tools = SteelTools(api_key=STEEL_API_KEY)agent = Agent(name="Web Scraper",model=OpenAIChat(id="gpt-5-nano", api_key=OPENAI_API_KEY),tools=[tools],instructions=["Extract content clearly and format nicely","Always close sessions when done",],markdown=True,)response = agent.run(TASK)
agent.run starts a loop: read the task, pick a tool, call it, read the result, decide the next step. Steel is invisible to the model. It sees four Python methods (navigate_to, screenshot, get_page_content, close_session) and calls them as it works.
The toolkit contract
SteelTools subclasses agno.tools.Toolkit. The constructor appends bound methods to a tools list and passes it to super().__init__:
tools: List[Any] = []tools.append(self.navigate_to)tools.append(self.screenshot)tools.append(self.get_page_content)tools.append(self.close_session)super().__init__(name="steel_tools", tools=tools, **kwargs)
Each method looks like a plain Python function. Agno reads the signature and docstring to build the schema:
def navigate_to(self, url: str, connect_url: Optional[str] = None) -> str:"""Navigates to a URL.Args:url (str): The URL to navigate toconnect_url (str, optional): The connection URL from an existing sessionReturns:JSON string with navigation status"""
That is the whole contract. The model sees navigate_to(url, connect_url=None) with the docstring as description, and calls it with arguments it picks itself.
Lazy sessions
_ensure_session and _initialize_browser defer the Steel session and Playwright connection until the first tool call. If the task turns out to be answerable without a browser, no session is ever created and nothing is billed.
def _ensure_session(self):if not self._session:self._session = self.client.sessions.create()self._connect_url = f"{self._session.websocket_url}&apiKey={self.api_key}"
When _initialize_browser fires, it calls sync_playwright().start(), connects over CDP with chromium.connect_over_cdp(self._connect_url), reuses the existing context and page Steel already opened, and caches everything on self. Every subsequent tool call picks up the same browser handle.
Run it
cd examples/agnocp .env.example .env # set STEEL_API_KEY and OPENAI_API_KEYuv run main.py
Get keys from app.steel.dev and platform.openai.com. The default task scrapes quotes.toscrape.com across two pages.
Your output varies. Structure looks like this:
Steel + Agno Starter============================================================Results:**Page 1:**1. "The world as we have created it is a process of our thinking..." - Albert Einstein2. "It is our choices, Harry, that show what we truly are..." - J.K. Rowling3. "There are only two ways to live your life..." - Albert Einstein**Page 2:**4. "This life is what you make it..." - Marilyn Monroe5. "It takes courage to grow up and become who you really are." - E.E. CummingsDone!
A run is a handful of tool calls (two navigate_to, two get_page_content, one close_session) plus whatever reasoning tokens gpt-5-nano spends in between. Expect ~30 seconds and a few cents of Steel session time.
Cleanup matters
Steel bills per session-minute. The finally block in main() calls tools.close_session(), which releases the session even when the agent crashes mid-run:
try:response = agent.run(TASK)...finally:tools.close_session()
close_session tears down Playwright and the browser handle via _cleanup, then calls self.client.sessions.release(self._session.id) so Steel can shut down the remote Chrome. Skipping either keeps the session live until Steel's 5-minute idle timeout fires.
Make it yours
- Change the task. Set
TASKin.envor edit the default inmain.py. Something like "Go to example.com, log in with these credentials, extract the invoice table." The agent sequences tool calls on its own. - Add tools. Append any method to the
toolslist inSteelTools.__init__:click_selector(selector),fill_form(field, value),wait_for_text(text). Typed signature plus docstring, Agno handles the rest. - Turn on stealth. Pass flags to
self.client.sessions.create()inside_ensure_session:use_proxy=True,solve_captcha=True,session_timeout=600000. - Swap the model.
OpenAIChat(id="gpt-5-nano", ...)is cheap and fast. Agno also shipsagno.models.anthropic.Claudeand others; the toolkit stays the same.
Related
Related recipes
Build a browser agent with the Claude Agent SDK
Use Steel with the Claude Agent SDK (TypeScript) to build a tool-using browser agent on Anthropic's first-party agent loop.
Build a typed browser agent with Pydantic AI
Use Steel with Pydantic AI to build typed, provider-agnostic browser agents with dependency injection.
Build a typed browser agent with LangGraph
Use Steel with LangGraph to build a typed browser agent with an explicit state-machine loop and a structured-output formatter node.