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

Step 1: Project Setup

Create a Typescript project and starter files.

Terminal
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 .env
npm install steel-sdk @inngest/agent-kit zod playwright dotenv

Add your API keys to .env:

ENV
.env
1
STEEL_API_KEY=your-steel-api-key-here
2
OPENAI_API_KEY=your-openai-api-key-here

Step 2: Create a browsing tool

We’ll define a custom AgentKit tool

Typescript
index.ts
1
import dotenv from "dotenv";
2
dotenv.config();
3
4
import { z } from "zod";
5
import { chromium } from "playwright";
6
import Steel from "steel-sdk";
7
import {
8
openai,
9
createAgent,
10
createNetwork,
11
createTool,
12
} from "@inngest/agent-kit";
13
14
const STEEL_API_KEY = process.env.STEEL_API_KEY || "your-steel-api-key-here";
15
const OPENAI_API_KEY = process.env.OPENAI_API_KEY || "your-openai-api-key-here";
16
17
const client = new Steel({ steelAPIKey: STEEL_API_KEY });
18
19
const browseHackerNews = createTool({
20
name: "browse_hacker_news",
21
description:
22
"Fetch Hacker News stories (top/best/new) and optionally filter by topics",
23
parameters: z.object({
24
section: z.enum(["top", "best", "new"]).default("top"),
25
topics: z.array(z.string()).optional(),
26
limit: z.number().int().min(1).max(20).default(5),
27
}),
28
handler: async ({ section, topics, limit }, { step }) => {
29
if (STEEL_API_KEY === "your-steel-api-key-here") {
30
throw new Error("Set STEEL_API_KEY");
31
}
32
return await step?.run("browse-hn", async () => {
33
const session = await client.sessions.create({});
34
const browser = await chromium.connectOverCDP(
35
`${session.websocketUrl}&apiKey=${STEEL_API_KEY}`
36
);
37
try {
38
const context = browser.contexts()[0];
39
const page = context.pages()[0];
40
const base = "https://news.ycombinator.com";
41
const url =
42
section === "best"
43
? `${base}/best`
44
: section === "new"
45
? `${base}/newest`
46
: base;
47
48
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
49
50
// Extract rows client-side for speed & resilience
51
const items = await page.evaluate((maxItems: number) => {
52
const rows = Array.from(document.querySelectorAll("tr.athing"));
53
const take = Math.min(maxItems * 2, rows.length);
54
const out = [] as Array<{
55
rank: number;
56
title: string;
57
url: string;
58
site: string | null;
59
points: number;
60
comments: number;
61
itemId: string;
62
}>;
63
for (let i = 0; i < take; i++) {
64
const row = rows[i] as HTMLElement;
65
const titleEl = row.querySelector(
66
".titleline > a"
67
) as HTMLAnchorElement | null;
68
const sub = row.nextElementSibling as HTMLElement | null;
69
const scoreEl = sub?.querySelector(".score");
70
const commentsLink = sub?.querySelector(
71
'a[href*="item?id="]:last-child'
72
) as HTMLAnchorElement | null;
73
74
const rankText = row.querySelector(".rank")?.textContent || "";
75
const rank =
76
parseInt(rankText.replace(".", "").trim(), 10) || i + 1;
77
const title = titleEl?.textContent?.trim() || "";
78
const url = titleEl?.getAttribute("href") || "";
79
const site = row.querySelector(".sitestr")?.textContent || null;
80
const points = scoreEl?.textContent
81
? parseInt(scoreEl.textContent, 10)
82
: 0;
83
const commentsText = commentsLink?.textContent || "";
84
const commentsNum = /\d+/.test(commentsText)
85
? parseInt((commentsText.match(/\d+/) || ["0"])[0], 10)
86
: 0;
87
const itemId = row.getAttribute("id") || "";
88
out.push({ rank, title, url, site, points, comments: commentsNum, itemId });
89
}
90
return out;
91
}, limit);
92
93
// Optional topic filtering, then dedupe + cap
94
const filtered =
95
topics && topics.length > 0
96
? items.filter((it) => {
97
const t = it.title.toLowerCase();
98
return topics.some((kw) => t.includes(kw.toLowerCase()));
99
})
100
: items;
101
102
const deduped: typeof filtered = [];
103
const seen = new Set<string>();
104
for (const it of filtered) {
105
const key = `${it.title}|${it.url}`;
106
if (!seen.has(key)) {
107
seen.add(key);
108
deduped.push(it);
109
}
110
if (deduped.length >= limit) break;
111
}
112
return deduped.slice(0, limit);
113
} finally {
114
// Always clean up cloud resources
115
try {
116
await browser.close();
117
} finally {
118
await 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.

Typescript
index.ts
1
const hnAgent = createAgent({
2
name: "hn_curator",
3
description: "Curates interesting Hacker News stories by topic",
4
system:
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.",
6
tools: [browseHackerNews],
7
});
8
9
const hnNetwork = createNetwork({
10
name: "hacker-news-network",
11
description: "Network for curating Hacker News stories",
12
agents: [hnAgent],
13
maxIter: 2,
14
defaultModel: openai({
15
model: "gpt-4o-mini",
16
}),
17
});

Step 5: Run the network

Add a small main() that checks env vars, runs the network, and prints results.

Typescript
index.ts
1
async function main() {
2
console.log("🚀 Steel + Agent Kit Starter");
3
console.log("=".repeat(60));
4
5
if (STEEL_API_KEY === "your-steel-api-key-here") {
6
console.warn("⚠️ WARNING: Please replace 'your-steel-api-key-here' with your actual Steel API key");
7
console.warn(" Get your API key at: https://app.steel.dev/settings/api-keys");
8
return;
9
}
10
if (OPENAI_API_KEY === "your-openai-api-key-here") {
11
console.warn("⚠️ WARNING: Please replace 'your-openai-api-key-here' with your actual OpenAI API key");
12
console.warn(" Get your API key at: https://platform.openai.com/api-keys");
13
return;
14
}
15
16
try {
17
console.log("\nRunning HN curation...");
18
const 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
);
21
const results = (run as any).state?.results ?? [];
22
console.log("\nResults:\n" + JSON.stringify(results, null, 2));
23
} catch (err) {
24
console.error("An error occurred:", err);
25
} finally {
26
console.log("Done!");
27
}
28
}
29
30
main();

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:

Typescript
index.ts
1
import dotenv from "dotenv";
2
dotenv.config();
3
import { z } from "zod";
4
import { chromium } from "playwright";
5
import Steel from "steel-sdk";
6
import {
7
openai,
8
createAgent,
9
createNetwork,
10
createTool,
11
} from "@inngest/agent-kit";
12
13
// Replace with your own API keys
14
const STEEL_API_KEY = process.env.STEEL_API_KEY || "your-steel-api-key-here";
15
const OPENAI_API_KEY = process.env.OPENAI_API_KEY || "your-openai-api-key-here";
16
17
const client = new Steel({ steelAPIKey: STEEL_API_KEY });
18
19
const browseHackerNews = createTool({
20
name: "browse_hacker_news",
21
description:
22
"Fetch Hacker News stories (top/best/new) and optionally filter by topics",
23
parameters: z.object({
24
section: z.enum(["top", "best", "new"]).default("top"),
25
topics: z.array(z.string()).optional(),
26
limit: z.number().int().min(1).max(20).default(5),
27
}),
28
handler: async ({ section, topics, limit }, { step }) => {
29
if (STEEL_API_KEY === "your-steel-api-key-here") {
30
throw new Error("Set STEEL_API_KEY");
31
}
32
return await step?.run("browse-hn", async () => {
33
const session = await client.sessions.create({});
34
const browser = await chromium.connectOverCDP(
35
`${session.websocketUrl}&apiKey=${STEEL_API_KEY}`
36
);
37
try {
38
const context = browser.contexts()[0];
39
const page = context.pages()[0];
40
const base = "https://news.ycombinator.com";
41
const url =
42
section === "best"
43
? `${base}/best`
44
: section === "new"
45
? `${base}/newest`
46
: base;
47
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
48
const items = await page.evaluate((maxItems: number) => {
49
const rows = Array.from(document.querySelectorAll("tr.athing"));
50
const take = Math.min(maxItems * 2, rows.length);
51
const out = [] as Array<{
52
rank: number;
53
title: string;
54
url: string;
55
site: string | null;
56
points: number;
57
comments: number;
58
itemId: string;
59
}>;
60
for (let i = 0; i < take; i++) {
61
const row = rows[i] as HTMLElement;
62
const titleEl = row.querySelector(
63
".titleline > a"
64
) as HTMLAnchorElement | null;
65
const sub = row.nextElementSibling as HTMLElement | null;
66
const scoreEl = sub?.querySelector(".score");
67
const commentsLink = sub?.querySelector(
68
'a[href*="item?id="]:last-child'
69
) as HTMLAnchorElement | null;
70
const rankText = row.querySelector(".rank")?.textContent || "";
71
const rank =
72
parseInt(rankText.replace(".", "").trim(), 10) || i + 1;
73
const title = titleEl?.textContent?.trim() || "";
74
const url = titleEl?.getAttribute("href") || "";
75
const site = row.querySelector(".sitestr")?.textContent || null;
76
const points = scoreEl?.textContent
77
? parseInt(scoreEl.textContent, 10)
78
: 0;
79
const commentsText = commentsLink?.textContent || "";
80
const commentsNum = /\d+/.test(commentsText)
81
? parseInt((commentsText.match(/\d+/) || ["0"])[0], 10)
82
: 0;
83
const itemId = row.getAttribute("id") || "";
84
out.push({
85
rank,
86
title,
87
url,
88
site,
89
points,
90
comments: commentsNum,
91
itemId,
92
});
93
}
94
return out;
95
}, limit);
96
const filtered =
97
topics && topics.length > 0
98
? items.filter((it) => {
99
const t = it.title.toLowerCase();
100
return topics.some((kw) => t.includes(kw.toLowerCase()));
101
})
102
: items;
103
const deduped = [] as typeof filtered;
104
const seen = new Set<string>();
105
for (const it of filtered) {
106
const key = `${it.title}|${it.url}`;
107
if (!seen.has(key)) {
108
seen.add(key);
109
deduped.push(it);
110
}
111
if (deduped.length >= limit) break;
112
}
113
return deduped.slice(0, limit);
114
} finally {
115
try {
116
await browser.close();
117
} finally {
118
await client.sessions.release(session.id);
119
}
120
}
121
});
122
},
123
});
124
125
const hnAgent = createAgent({
126
name: "hn_curator",
127
description: "Curates interesting Hacker News stories by topic",
128
system:
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.",
130
tools: [browseHackerNews],
131
});
132
133
const hnNetwork = createNetwork({
134
name: "hacker-news-network",
135
description: "Network for curating Hacker News stories",
136
agents: [hnAgent],
137
maxIter: 2,
138
defaultModel: openai({
139
model: "gpt-4o-mini",
140
}),
141
});
142
143
async function main() {
144
console.log("🚀 Steel + Agent Kit Starter");
145
console.log("=".repeat(60));
146
147
if (STEEL_API_KEY === "your-steel-api-key-here") {
148
console.warn(
149
"⚠️ WARNING: Please replace 'your-steel-api-key-here' with your actual Steel API key"
150
);
151
console.warn(
152
" Get your API key at: https://app.steel.dev/settings/api-keys"
153
);
154
return;
155
}
156
157
if (OPENAI_API_KEY === "your-openai-api-key-here") {
158
console.warn(
159
"⚠️ WARNING: Please replace 'your-openai-api-key-here' with your actual OpenAI API key"
160
);
161
console.warn(
162
" Get your API key at: https://platform.openai.com/api-keys"
163
);
164
return;
165
}
166
167
try {
168
console.log("\nRunning HN curation...");
169
const 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
);
172
const results = (run as any).state?.results ?? [];
173
console.log("\nResults:\n" + JSON.stringify(results, null, 2));
174
} catch (err) {
175
console.error("An error occurred:", err);
176
} finally {
177
console.log("Done!");
178
}
179
}
180
181
main();
182

Customize the prompt

Try adjusting the network input:

Typescript
main.ts
1
await 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