Quickstart
This guide shows how to use AgentKit with Steel to build a small network that browses Hacker News in a live cloud browser via CDP, filters stories by topic, and returns concise picks.
Prerequisites
-
Node.js v20+
-
Steel API key (get one at app.steel.dev)
-
OpenAI API key (get one at platform.openai.com)
Step 1: Project Setup
Create a Typescript project and starter files.
mkdir steel-agentkit-hn && \cd steel-agentkit-hn && \npm init -y && \npm install -D typescript @types/node ts-node && \npx tsc --init && \npm pkg set scripts.start="ts-node index.ts" && \touch index.ts .envnpm install steel-sdk @inngest/agent-kit zod playwright dotenv
Add your API keys to .env
:
1STEEL_API_KEY=your-steel-api-key-here2OPENAI_API_KEY=your-openai-api-key-here
Step 2: Create a browsing tool
We’ll define a custom AgentKit tool
1import dotenv from "dotenv";2dotenv.config();34import { z } from "zod";5import { chromium } from "playwright";6import Steel from "steel-sdk";7import {8openai,9createAgent,10createNetwork,11createTool,12} from "@inngest/agent-kit";1314const STEEL_API_KEY = process.env.STEEL_API_KEY || "your-steel-api-key-here";15const OPENAI_API_KEY = process.env.OPENAI_API_KEY || "your-openai-api-key-here";1617const client = new Steel({ steelAPIKey: STEEL_API_KEY });1819const browseHackerNews = createTool({20name: "browse_hacker_news",21description:22"Fetch Hacker News stories (top/best/new) and optionally filter by topics",23parameters: z.object({24section: z.enum(["top", "best", "new"]).default("top"),25topics: z.array(z.string()).optional(),26limit: z.number().int().min(1).max(20).default(5),27}),28handler: async ({ section, topics, limit }, { step }) => {29if (STEEL_API_KEY === "your-steel-api-key-here") {30throw new Error("Set STEEL_API_KEY");31}32return await step?.run("browse-hn", async () => {33const session = await client.sessions.create({});34const browser = await chromium.connectOverCDP(35`${session.websocketUrl}&apiKey=${STEEL_API_KEY}`36);37try {38const context = browser.contexts()[0];39const page = context.pages()[0];40const base = "https://news.ycombinator.com";41const url =42section === "best"43? `${base}/best`44: section === "new"45? `${base}/newest`46: base;4748await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });4950// Extract rows client-side for speed & resilience51const items = await page.evaluate((maxItems: number) => {52const rows = Array.from(document.querySelectorAll("tr.athing"));53const take = Math.min(maxItems * 2, rows.length);54const out = [] as Array<{55rank: number;56title: string;57url: string;58site: string | null;59points: number;60comments: number;61itemId: string;62}>;63for (let i = 0; i < take; i++) {64const row = rows[i] as HTMLElement;65const titleEl = row.querySelector(66".titleline > a"67) as HTMLAnchorElement | null;68const sub = row.nextElementSibling as HTMLElement | null;69const scoreEl = sub?.querySelector(".score");70const commentsLink = sub?.querySelector(71'a[href*="item?id="]:last-child'72) as HTMLAnchorElement | null;7374const rankText = row.querySelector(".rank")?.textContent || "";75const rank =76parseInt(rankText.replace(".", "").trim(), 10) || i + 1;77const title = titleEl?.textContent?.trim() || "";78const url = titleEl?.getAttribute("href") || "";79const site = row.querySelector(".sitestr")?.textContent || null;80const points = scoreEl?.textContent81? parseInt(scoreEl.textContent, 10)82: 0;83const commentsText = commentsLink?.textContent || "";84const commentsNum = /\d+/.test(commentsText)85? parseInt((commentsText.match(/\d+/) || ["0"])[0], 10)86: 0;87const itemId = row.getAttribute("id") || "";88out.push({ rank, title, url, site, points, comments: commentsNum, itemId });89}90return out;91}, limit);9293// Optional topic filtering, then dedupe + cap94const filtered =95topics && topics.length > 096? items.filter((it) => {97const t = it.title.toLowerCase();98return topics.some((kw) => t.includes(kw.toLowerCase()));99})100: items;101102const deduped: typeof filtered = [];103const seen = new Set<string>();104for (const it of filtered) {105const key = `${it.title}|${it.url}`;106if (!seen.has(key)) {107seen.add(key);108deduped.push(it);109}110if (deduped.length >= limit) break;111}112return deduped.slice(0, limit);113} finally {114// Always clean up cloud resources115try {116await browser.close();117} finally {118await client.sessions.release(session.id);119}120}121});122},123});124
Step 3: Build the Agenth & Network
Wire the tool into an agent and run it inside a small network with your default model.
1const hnAgent = createAgent({2name: "hn_curator",3description: "Curates interesting Hacker News stories by topic",4system:5"Surface novel, high-signal Hacker News stories. Favor technical depth, originality, and relevance to requested topics. Use the tool to browse and return concise picks.",6tools: [browseHackerNews],7});89const hnNetwork = createNetwork({10name: "hacker-news-network",11description: "Network for curating Hacker News stories",12agents: [hnAgent],13maxIter: 2,14defaultModel: openai({15model: "gpt-4o-mini",16}),17});
Step 5: Run the network
Add a small main()
that checks env vars, runs the network, and prints results.
1async function main() {2console.log("🚀 Steel + Agent Kit Starter");3console.log("=".repeat(60));45if (STEEL_API_KEY === "your-steel-api-key-here") {6console.warn("⚠️ WARNING: Please replace 'your-steel-api-key-here' with your actual Steel API key");7console.warn(" Get your API key at: https://app.steel.dev/settings/api-keys");8return;9}10if (OPENAI_API_KEY === "your-openai-api-key-here") {11console.warn("⚠️ WARNING: Please replace 'your-openai-api-key-here' with your actual OpenAI API key");12console.warn(" Get your API key at: https://platform.openai.com/api-keys");13return;14}1516try {17console.log("\nRunning HN curation...");18const run = await hnNetwork.run(19"Curate 5 interesting Hacker News stories about AI, TypeScript, and tooling. Prefer 'best' if relevant. Return title, url, points."20);21const results = (run as any).state?.results ?? [];22console.log("\nResults:\n" + JSON.stringify(results, null, 2));23} catch (err) {24console.error("An error occurred:", err);25} finally {26console.log("Done!");27}28}2930main();
Run it:
Open your console output to see your curated results. You can also watch the live Steel session from your Steel dashboard.
Complete Example
Paste the full index.ts below and run npm run start
:
1import dotenv from "dotenv";2dotenv.config();3import { z } from "zod";4import { chromium } from "playwright";5import Steel from "steel-sdk";6import {7openai,8createAgent,9createNetwork,10createTool,11} from "@inngest/agent-kit";1213// Replace with your own API keys14const STEEL_API_KEY = process.env.STEEL_API_KEY || "your-steel-api-key-here";15const OPENAI_API_KEY = process.env.OPENAI_API_KEY || "your-openai-api-key-here";1617const client = new Steel({ steelAPIKey: STEEL_API_KEY });1819const browseHackerNews = createTool({20name: "browse_hacker_news",21description:22"Fetch Hacker News stories (top/best/new) and optionally filter by topics",23parameters: z.object({24section: z.enum(["top", "best", "new"]).default("top"),25topics: z.array(z.string()).optional(),26limit: z.number().int().min(1).max(20).default(5),27}),28handler: async ({ section, topics, limit }, { step }) => {29if (STEEL_API_KEY === "your-steel-api-key-here") {30throw new Error("Set STEEL_API_KEY");31}32return await step?.run("browse-hn", async () => {33const session = await client.sessions.create({});34const browser = await chromium.connectOverCDP(35`${session.websocketUrl}&apiKey=${STEEL_API_KEY}`36);37try {38const context = browser.contexts()[0];39const page = context.pages()[0];40const base = "https://news.ycombinator.com";41const url =42section === "best"43? `${base}/best`44: section === "new"45? `${base}/newest`46: base;47await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });48const items = await page.evaluate((maxItems: number) => {49const rows = Array.from(document.querySelectorAll("tr.athing"));50const take = Math.min(maxItems * 2, rows.length);51const out = [] as Array<{52rank: number;53title: string;54url: string;55site: string | null;56points: number;57comments: number;58itemId: string;59}>;60for (let i = 0; i < take; i++) {61const row = rows[i] as HTMLElement;62const titleEl = row.querySelector(63".titleline > a"64) as HTMLAnchorElement | null;65const sub = row.nextElementSibling as HTMLElement | null;66const scoreEl = sub?.querySelector(".score");67const commentsLink = sub?.querySelector(68'a[href*="item?id="]:last-child'69) as HTMLAnchorElement | null;70const rankText = row.querySelector(".rank")?.textContent || "";71const rank =72parseInt(rankText.replace(".", "").trim(), 10) || i + 1;73const title = titleEl?.textContent?.trim() || "";74const url = titleEl?.getAttribute("href") || "";75const site = row.querySelector(".sitestr")?.textContent || null;76const points = scoreEl?.textContent77? parseInt(scoreEl.textContent, 10)78: 0;79const commentsText = commentsLink?.textContent || "";80const commentsNum = /\d+/.test(commentsText)81? parseInt((commentsText.match(/\d+/) || ["0"])[0], 10)82: 0;83const itemId = row.getAttribute("id") || "";84out.push({85rank,86title,87url,88site,89points,90comments: commentsNum,91itemId,92});93}94return out;95}, limit);96const filtered =97topics && topics.length > 098? items.filter((it) => {99const t = it.title.toLowerCase();100return topics.some((kw) => t.includes(kw.toLowerCase()));101})102: items;103const deduped = [] as typeof filtered;104const seen = new Set<string>();105for (const it of filtered) {106const key = `${it.title}|${it.url}`;107if (!seen.has(key)) {108seen.add(key);109deduped.push(it);110}111if (deduped.length >= limit) break;112}113return deduped.slice(0, limit);114} finally {115try {116await browser.close();117} finally {118await client.sessions.release(session.id);119}120}121});122},123});124125const hnAgent = createAgent({126name: "hn_curator",127description: "Curates interesting Hacker News stories by topic",128system:129"Surface novel, high-signal Hacker News stories. Favor technical depth, originality, and relevance to requested topics. Use the tool to browse and return concise picks.",130tools: [browseHackerNews],131});132133const hnNetwork = createNetwork({134name: "hacker-news-network",135description: "Network for curating Hacker News stories",136agents: [hnAgent],137maxIter: 2,138defaultModel: openai({139model: "gpt-4o-mini",140}),141});142143async function main() {144console.log("🚀 Steel + Agent Kit Starter");145console.log("=".repeat(60));146147if (STEEL_API_KEY === "your-steel-api-key-here") {148console.warn(149"⚠️ WARNING: Please replace 'your-steel-api-key-here' with your actual Steel API key"150);151console.warn(152" Get your API key at: https://app.steel.dev/settings/api-keys"153);154return;155}156157if (OPENAI_API_KEY === "your-openai-api-key-here") {158console.warn(159"⚠️ WARNING: Please replace 'your-openai-api-key-here' with your actual OpenAI API key"160);161console.warn(162" Get your API key at: https://platform.openai.com/api-keys"163);164return;165}166167try {168console.log("\nRunning HN curation...");169const run = await hnNetwork.run(170"Curate 5 interesting Hacker News stories about AI, TypeScript, and tooling. Prefer 'best' if relevant. Return title, url, points."171);172const results = (run as any).state?.results ?? [];173console.log("\nResults:\n" + JSON.stringify(results, null, 2));174} catch (err) {175console.error("An error occurred:", err);176} finally {177console.log("Done!");178}179}180181main();182
Customize the prompt
Try adjusting the network input:
1await hnNetwork.run(2"Curate 8 stories about WebAssembly, Edge runtimes, and performance. Use 'new' if there are fresh posts. Return title, url, site, points, comments."3);
Next steps
-
AgentKit Docs: https://agentkit.inngest.com/overview
-
Examples Gallery: https://agentkit.inngest.com/examples/overview
-
Steel Sessions API: /overview/sessions-api/overview
-
Session Lifecycle: https://docs.steel.dev/overview/sessions-api/session-lifecycle
-
Steel Node SDK: https://github.com/steel-dev/steel-node