Quickstart
This guide walks you through connecting Agno with Steel by adding a Playwright-powered Steel toolkit and running an agent that browses and extracts content from live websites.
Prerequisites
Make sure you have:
-
Python 3.11+
-
Steel API key (get one at app.steel.dev)
-
(Optional) OpenAI API key if your Agno setup uses OpenAI models
Step 1: Project setup
Create and activate a virtual environment, then install dependencies:
# Create projectmkdir steel-agno-startercd steel-agno-starter# (Recommended) Create & activate a virtual environmentpython3 -m venv .venvsource .venv/bin/activate # On Windows: .venv\Scripts\activate# Create filestouch main.py .env# Install dependenciespip install agno steel-sdk python-dotenv playwright
Create a .env
file with your keys and a default task:
1STEEL_API_KEY=your_steel_api_key_here2OPENAI_API_KEY=your_openai_api_key_here # optional, if your Agno model needs it3TASK=Go to https://quotes.toscrape.com and: 1. Get the first 3 quotes with authors 2. Navigate to page 2 3. Get 2 more quotes from page 2
Step 2: Add a Steel toolkit and run an Agno Agent
First, define a toolkit that wraps Steel’s browser sessions and Playwright.
1import os2import json3from typing import Any, Dict, List, Optional4from agno.tools import Toolkit5from agno.utils.log import log_debug, logger6from playwright.sync_api import sync_playwright7from steel import Steel8910class SteelTools(Toolkit):11def __init__(12self,13api_key: Optional[str] = None,14**kwargs,15):16"""Initialize SteelTools.1718Args:19api_key (str, optional): Steel API key (defaults to STEEL_API_KEY env var).20"""21self.api_key = api_key or os.getenv("STEEL_API_KEY")22if not self.api_key:23raise ValueError(24"STEEL_API_KEY is required. Please set the STEEL_API_KEY environment variable."25)2627self.client = Steel(steel_api_key=self.api_key)2829self._playwright = None30self._browser = None31self._page = None32self._session = None33self._connect_url = None3435tools: List[Any] = []36tools.append(self.navigate_to)37tools.append(self.screenshot)38tools.append(self.get_page_content)39tools.append(self.close_session)4041super().__init__(name="steel_tools", tools=tools, **kwargs)4243def _ensure_session(self):44"""Ensures a Steel session exists, creating one if needed."""45if not self._session:46try:47self._session = self.client.sessions.create() # type: ignore48if self._session:49self._connect_url = f"{self._session.websocket_url}&apiKey={self.api_key}" # type: ignore50log_debug(f"Created new Steel session with ID: {self._session.id}")51except Exception as e:52logger.error(f"Failed to create Steel session: {str(e)}")53raise5455def _initialize_browser(self, connect_url: Optional[str] = None):56"""57Initialize browser connection if not already initialized.58Use provided connect_url or ensure we have a session with a connect_url59"""60if connect_url:61self._connect_url = connect_url if connect_url else "" # type: ignore62elif not self._connect_url:63self._ensure_session()6465if not self._playwright:66self._playwright = sync_playwright().start() # type: ignore67if self._playwright:68self._browser = self._playwright.chromium.connect_over_cdp(self._connect_url)69context = self._browser.contexts[0] if self._browser else ""70self._page = context.pages[0] or context.new_page() # type: ignore7172def _cleanup(self):73"""Clean up browser resources."""74if self._browser:75self._browser.close()76self._browser = None77if self._playwright:78self._playwright.stop()79self._playwright = None80self._page = None8182def _create_session(self) -> Dict[str, str]:83"""Creates a new Steel browser session.8485Returns:86Dictionary containing session details including session_id and connect_url.87"""88self._ensure_session()89return {90"session_id": self._session.id if self._session else "",91"connect_url": self._connect_url or "",92}9394def navigate_to(self, url: str, connect_url: Optional[str] = None) -> str:95"""Navigates to a URL.9697Args:98url (str): The URL to navigate to99connect_url (str, optional): The connection URL from an existing session100101Returns:102JSON string with navigation status103"""104try:105self._initialize_browser(connect_url)106if self._page:107self._page.goto(url, wait_until="networkidle")108result = {"status": "complete", "title": self._page.title() if self._page else "", "url": url}109return json.dumps(result)110except Exception as e:111self._cleanup()112raise e113114def screenshot(self, path: str, full_page: bool = True, connect_url: Optional[str] = None) -> str:115"""Takes a screenshot of the current page.116117Args:118path (str): Where to save the screenshot119full_page (bool): Whether to capture the full page120connect_url (str, optional): The connection URL from an existing session121122Returns:123JSON string confirming screenshot was saved124"""125try:126self._initialize_browser(connect_url)127if self._page:128self._page.screenshot(path=path, full_page=full_page)129return json.dumps({"status": "success", "path": path})130except Exception as e:131self._cleanup()132raise e133134def get_page_content(self, connect_url: Optional[str] = None) -> str:135"""Gets the HTML content of the current page.136137Args:138connect_url (str, optional): The connection URL from an existing session139140Returns:141The page HTML content142"""143try:144self._initialize_browser(connect_url)145return self._page.content() if self._page else ""146except Exception as e:147self._cleanup()148raise e149150def close_session(self) -> str:151"""Closes the current Steel browser session and cleans up resources.152153Returns:154JSON string with closure status155"""156try:157self._cleanup()158159try:160if self._session:161self.client.sessions.release(self._session.id) # type: ignore162except Exception as release_error:163logger.warning(f"Failed to release Steel session: {str(release_error)}")164165self._session = None166self._connect_url = None167168return json.dumps(169{170"status": "closed",171"message": "Browser resources cleaned up. Steel session released if active.",172}173)174except Exception as e:175return json.dumps({"status": "warning", "message": f"Cleanup completed with warning: {str(e)}"})176
Step 3: Register a Steel toolkit and run an Agno Agent
Create an Agent that uses your toolkit to perform multi-step tasks.
1import os2from dotenv import load_dotenv3from agno.agent import Agent4from steel_tools import SteelTools56load_dotenv()78STEEL_API_KEY = os.getenv("STEEL_API_KEY") or "your-steel-api-key-here"9TASK = os.getenv("TASK") or "Go to https://quotes.toscrape.com and get some quotes"1011def main():12tools = SteelTools(api_key=STEEL_API_KEY)1314agent = Agent(15name="Web Scraper",16tools=[tools],17instructions=[18"Use the tools to browse and extract content.",19"Format results cleanly as markdown.",20"Always close sessions when done.",21],22markdown=True,23)2425response = agent.run(TASK)26print("\nResults:\n")27print(response.content)2829tools.close_session()3031if __name__ == "__main__":32main()33
Run it:
You’ll see the agent connect to a live Steel browser via CDP, navigate to the site, and extract content. A session viewer URL is printed in your Steel dashboard for live/replay views.
Complete Example
Paste the full script below into main.py
and run:
1import json2import os3from typing import Any, Dict, List, Optional45from agno.tools import Toolkit6from agno.utils.log import log_debug, logger7from agno.agent import Agent8from playwright.sync_api import sync_playwright9from steel import Steel1011from dotenv import load_dotenv1213load_dotenv()1415# Replace with your own API keys16STEEL_API_KEY = os.getenv("STEEL_API_KEY") or "your-steel-api-key-here"1718# Replace with your own task19TASK = os.getenv("TASK") or "Go to https://quotes.toscrape.com and: 1. Get the first 3 quotes with authors 2. Navigate to page 2 3. Get 2 more quotes from page 2"2021class SteelTools(Toolkit):22def __init__(23self,24api_key: Optional[str] = None,25**kwargs,26):27"""Initialize SteelTools.2829Args:30api_key (str, optional): Steel API key (defaults to STEEL_API_KEY env var).31"""32self.api_key = api_key or os.getenv("STEEL_API_KEY")33if not self.api_key:34raise ValueError(35"STEEL_API_KEY is required. Please set the STEEL_API_KEY environment variable."36)3738self.client = Steel(steel_api_key=self.api_key)3940self._playwright = None41self._browser = None42self._page = None43self._session = None44self._connect_url = None4546tools: List[Any] = []47tools.append(self.navigate_to)48tools.append(self.screenshot)49tools.append(self.get_page_content)50tools.append(self.close_session)5152super().__init__(name="steel_tools", tools=tools, **kwargs)5354def _ensure_session(self):55"""Ensures a Steel session exists, creating one if needed."""56if not self._session:57try:58self._session = self.client.sessions.create() # type: ignore59if self._session:60self._connect_url = f"{self._session.websocket_url}&apiKey={self.api_key}" # type: ignore61log_debug(f"Created new Steel session with ID: {self._session.id}")62except Exception as e:63logger.error(f"Failed to create Steel session: {str(e)}")64raise6566def _initialize_browser(self, connect_url: Optional[str] = None):67"""68Initialize browser connection if not already initialized.69Use provided connect_url or ensure we have a session with a connect_url70"""71if connect_url:72self._connect_url = connect_url if connect_url else "" # type: ignore73elif not self._connect_url:74self._ensure_session()7576if not self._playwright:77self._playwright = sync_playwright().start() # type: ignore78if self._playwright:79self._browser = self._playwright.chromium.connect_over_cdp(self._connect_url)80context = self._browser.contexts[0] if self._browser else ""81self._page = context.pages[0] or context.new_page() # type: ignore8283def _cleanup(self):84"""Clean up browser resources."""85if self._browser:86self._browser.close()87self._browser = None88if self._playwright:89self._playwright.stop()90self._playwright = None91self._page = None9293def _create_session(self) -> Dict[str, str]:94"""Creates a new Steel browser session.9596Returns:97Dictionary containing session details including session_id and connect_url.98"""99self._ensure_session()100return {101"session_id": self._session.id if self._session else "",102"connect_url": self._connect_url or "",103}104105def navigate_to(self, url: str, connect_url: Optional[str] = None) -> str:106"""Navigates to a URL.107108Args:109url (str): The URL to navigate to110connect_url (str, optional): The connection URL from an existing session111112Returns:113JSON string with navigation status114"""115try:116self._initialize_browser(connect_url)117if self._page:118self._page.goto(url, wait_until="networkidle")119result = {"status": "complete", "title": self._page.title() if self._page else "", "url": url}120return json.dumps(result)121except Exception as e:122self._cleanup()123raise e124125def screenshot(self, path: str, full_page: bool = True, connect_url: Optional[str] = None) -> str:126"""Takes a screenshot of the current page.127128Args:129path (str): Where to save the screenshot130full_page (bool): Whether to capture the full page131connect_url (str, optional): The connection URL from an existing session132133Returns:134JSON string confirming screenshot was saved135"""136try:137self._initialize_browser(connect_url)138if self._page:139self._page.screenshot(path=path, full_page=full_page)140return json.dumps({"status": "success", "path": path})141except Exception as e:142self._cleanup()143raise e144145def get_page_content(self, connect_url: Optional[str] = None) -> str:146"""Gets the HTML content of the current page.147148Args:149connect_url (str, optional): The connection URL from an existing session150151Returns:152The page HTML content153"""154try:155self._initialize_browser(connect_url)156return self._page.content() if self._page else ""157except Exception as e:158self._cleanup()159raise e160161def close_session(self) -> str:162"""Closes the current Steel browser session and cleans up resources.163164Returns:165JSON string with closure status166"""167try:168self._cleanup()169170try:171if self._session:172self.client.sessions.release(self._session.id) # type: ignore173except Exception as release_error:174logger.warning(f"Failed to release Steel session: {str(release_error)}")175176self._session = None177self._connect_url = None178179return json.dumps(180{181"status": "closed",182"message": "Browser resources cleaned up. Steel session released if active.",183}184)185except Exception as e:186return json.dumps({"status": "warning", "message": f"Cleanup completed with warning: {str(e)}"})187188def main():189print("🚀 Steel + Agno Starter")190print("=" * 60)191192if STEEL_API_KEY == "your-steel-api-key-here":193print("⚠️ WARNING: Please replace 'your-steel-api-key-here' with your actual Steel API key")194print(" Get your API key at: https://app.steel.dev/settings/api-keys")195return196197tools = SteelTools(api_key=STEEL_API_KEY)198agent = Agent(199name="Web Scraper",200tools=[tools],201instructions=[202"Extract content clearly and format nicely",203"Always close sessions when done",204],205markdown=True,206)207208try:209response = agent.run(TASK)210print("\nResults:\n")211print(response.content)212except Exception as e:213print(f"An error occurred: {e}")214finally:215tools.close_session()216print("Done!")217218if __name__ == "__main__":219main()220
Customizing your agent’s task
Try modifying the TASK
in your .env
:
1# Crawl a product page and extract specs2TASK=Go to https://example.com/product/123 and extract the product name, price, and 5 key specs.34# Capture a screenshot-only workflow5TASK=Go to https://news.ycombinator.com, take a full-page screenshot, and return the page title.67# Multi-step navigation8TASK=Open https://docs.steel.dev, search for "session lifecycle", and summarize the key steps with anchors.
Next Steps
-
Agno Docs: https://docs.agno.com
-
Session Lifecycles: https://docs.steel.dev/overview/sessions-api/session-lifecycle
-
Steel Sessions API: https://docs.steel.dev/overview/sessions-api/overview
-
Steel Python SDK: https://github.com/steel-dev/steel-python
-
Playwright Docs: https://playwright.dev/python/