Build a browser agent with Genkit
Use Steel with Genkit Go to build a tool-calling agent that navigates and extracts from a chromedp-backed browser and completes a web task.
Scaffolds a starter project locally. Requires the Steel CLI.
Genkit is Google's Go framework for building LLM applications. genkit.DefineTool turns a typed Go function into a tool the model can call, inferring the tool's JSON schema from the input struct by reflection. This starter defines three tools over a Steel cloud browser and lets a Claude model drive them to read Hacker News.
navigate := genkit.DefineTool(g, "navigate","Open a URL in the live browser tab and wait for it to load.",func(tc *ai.ToolContext, in navigateInput) (string, error) {var title, url stringerr := chromedp.Run(b.tab,chromedp.Navigate(in.URL), chromedp.Title(&title), chromedp.Location(&url))return fmt.Sprintf("title=%q url=%s", title, url), err},)resp, err := genkit.Generate(ctx, g,ai.WithModelName("anthropic/claude-haiku-4-5"),ai.WithTools(navigate, extract, scrape),ai.WithMaxTurns(12),ai.WithOutputType(Report{}),)
genkit.Generate runs the tool-calling loop for you. It calls the model, executes any tools the model requests, feeds the results back, and repeats until the model stops or WithMaxTurns is hit. You do not write the loop. WithOutputType(Report{}) constrains the final turn to a Go struct, so resp.Output(&out) fills a typed Report and a malformed answer is sent back for the model to correct.
The schema the model sees comes from struct tags. jsonschema_description on a field becomes that argument's description in the tool definition, which is how the model learns what rowSelector or attr mean:
type extractInput struct {RowSelector string `json:"rowSelector" jsonschema_description:"CSS selector matching each item, e.g. 'tr.athing'."`Fields []fieldSpec `json:"fields"`Limit int `json:"limit,omitempty"`}
Two ways to read a page
The tools cover the two access patterns a browsing agent needs:
navigate+extractdrive one live chromedp tab attached to the Steel session over CDP.extracttakes a row selector plus a field-per-column list and runs the whole pull inside a singlechromedp.Evaluate. Serial CDP round-trips to a cloud browser run about 200 to 300 ms each, so collapsing N rows by M fields into one evaluate keeps a page read under a second instead of stacking dozens of trips.scrapecallsclient.Scrapeand returns clean Markdown for a URL without touching the tab. It is the reliable path when the agent just needs an article's text, and it sidesteps selector guesswork entirely.
The model picks per step. On Hacker News it navigates, extracts the story rows, and answers. Pointed at an article it tends to reach for scrape.
Run it
cd examples/genkitcp .env.example .env # set STEEL_API_KEY and ANTHROPIC_API_KEYgo mod tidygo run .
Get keys from app.steel.dev and console.anthropic.com. The program prints a session viewer URL as it starts; open it in another tab to watch the browser run live. Each tool call prints its latency.
Your output varies. Structure looks like this:
Steel + Genkit Go Starter============================================================Session: https://app.steel.dev/sessions/ab12cd34...navigate: 1183msextract: 412ms (5 rows)Agent finished.{"summary": "The front page is mostly systems and AI tooling right now.","stories": [{"rank": 1,"title": "Show HN: ...","url": "https://example.com/...","points": "342"}]}tokens: 5120 in, 380 outReleasing Steel session...Session released. Replay: https://app.steel.dev/sessions/ab12cd34...
A run takes about 20 to 40 seconds and 4 to 8 model turns. Cost is a few cents of Steel session time plus Claude tokens. The deferred cleanup in main releases the session: Steel bills per session-minute, so a leaked session keeps running until the default 5-minute timeout.
Notes
- Go version. Genkit Go 1.0 requires Go 1.25, so
go.moddeclaresgo 1.25.0. chromedp is pinned tov0.13.6, the last release that still builds on Go 1.23, to keep the rest of the tree from pulling the toolchain higher than Genkit needs. - Session reuse. One Steel session and one chromedp tab live in the
browserstruct shared by every tool, sonavigateandextractact on the same page.chromedp.NoModifyURLstops chromedp rewriting Steel's websocket URL, which would drop theapiKeyquery parameter.
Make it yours
- Swap the model. Change
WithModelName. Any model the Anthropic plugin exposes works without code changes, for exampleanthropic/claude-sonnet-4-5. To use Gemini instead, register&googlegenai.GoogleAI{}ingenkit.Init, setGEMINI_API_KEY, and passgoogleai/gemini-2.5-flash. - Swap the task. Change the prompt and the
Reportstruct inmain. The tools stay the same; the agent re-plans against the new output shape. - Add a tool. Write a function
func(tc *ai.ToolContext, in In) (Out, error), wrap it withgenkit.DefineTool, and add it toWithTools. A useful fourth isclick(selector string)that runschromedp.Clickand waits for navigation. - Expose it as a flow. Wrap the
Generatecall ingenkit.DefineFlowto get tracing in the Genkit Dev UI and an HTTP handler for the same logic.
Related
Steel + Eino (Go) and Steel + Pydantic AI (Python) build the same agent shape in other frameworks. Genkit Go docs cover tools, flows, and plugins.
Related recipes
Run a durable browser agent with Restate
Build a Restate Virtual Object in TypeScript that uses durable OpenAI planning steps and Steel scraping to answer browser research questions.
Expose a Steel browser to any MCP client
Build a Model Context Protocol server in Go with the official SDK and chromedp that hands any MCP client a Steel cloud browser through explicit session-handle tools.
Build a browser agent with Eino
Use Steel with the ByteDance Eino framework to build a ReAct agent that calls Steel's scrape API as a tool to research and answer a web question.