Your first agent in 30 minutes
Module 02 · est. 35 min · You’ll build: a real, running agent that does one job in your business — triggered, model-driven, taking an actual action — by copying my starter repo.
TL;DR: You’re going to ship a working agent today by cloning a repo, dropping in an API key, writing one prompt file, and running one command. No framework, no Python, no vector database. The whole thing is a shell script that calls Claude, a prompt that tells it what to do, and a tool or two that lets it act. Thirty minutes from git clone to “holy crap, it actually did the thing.”
[Operator’s read] Every agent I run started exactly like this — small, ugly, and working. I didn’t architect them. I copied the last one, changed the prompt, swapped a tool, and shipped. This module is me handing you the “last one” so you can start at copy-paste instead of blank-page.
What we’re building
The agent from your Module 01 lab. If you skipped the lab, do it now — you need one yes/yes task (real action, runs unattended). For this walkthrough I’ll use a simple, universal one so you can follow exactly: a “daily digest” agent that reads a source (your calendar, a feed, a file), has Claude summarize what matters, and emails or Slacks it to you every morning. It’s the skeleton of my actual morning-brief agent. Once you can build this, you can build anything in the course — they’re all this shape with different organs.
Step 0 — What you need (5 minutes)
- Node 20+ and git. Check:
node -vandgit --version. - An Anthropic API key. Get one at console.anthropic.com, create a key, drop $5 of credit on it. (This agent will cost you fractions of a cent per run. The $5 is so you stop thinking about it.)
- Claude Code installed (optional but recommended):
npm install -g @anthropic-ai/claude-code. We’ll use it as our cockpit.
That’s the entire toolchain. No Docker, no Python virtualenv, no cloud account yet.
Step 1 — Copy the repo
git clone https://github.com/alerioja/agent-starter.git my-first-agent
cd my-first-agent
npm install
cp .env.example .envOpen .env and paste your key:
ANTHROPIC_API_KEY=sk-ant-...The repo is deliberately tiny. Here’s the whole structure:
my-first-agent/
.env
package.json
src/
run.ts ← the loop. you barely touch this.
prompt.md ← the agent's job description. you OWN this.
tools.ts ← the things the agent can DO. you add to this.
README.mdThree files matter. run.ts is the engine — copy it forever, rarely edit it. prompt.md is where the agent’s brain lives. tools.ts is its hands. That split is the whole philosophy: the loop is boilerplate, the prompt is the product, the tools are the leverage.
Step 2 — Understand the loop (don’t skip this)
Here’s the heart of run.ts. This is the entire engine, and it’s worth reading every line once so the magic stops being magic:
import Anthropic from '@anthropic-ai/sdk';
import { readFileSync } from 'fs';
import { tools, runTool } from './tools';
const client = new Anthropic(); // reads ANTHROPIC_API_KEY from env
const systemPrompt = readFileSync('./src/prompt.md', 'utf-8');
async function main() {
const messages: Anthropic.MessageParam[] = [{ role: 'user', content: 'Run your job now.' }];
// The agent loop: keep going until the model stops asking for tools.
while (true) {
const res = await client.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 2048,
system: systemPrompt,
tools, // what the agent is allowed to DO
messages,
});
messages.push({ role: 'assistant', content: res.content });
// Did the model decide to use a tool?
const toolUses = res.content.filter((c) => c.type === 'tool_use');
if (toolUses.length === 0) {
// No tool requested → the agent is done. Print its final word.
const text = res.content.find((c) => c.type === 'text');
console.log(text?.text ?? '(done)');
break;
}
// Run each requested tool, feed results back in, loop again.
const results = await Promise.all(
toolUses.map(async (t) => ({
type: 'tool_result' as const,
tool_use_id: t.id,
content: await runTool(t.name, t.input),
})),
);
messages.push({ role: 'user', content: results });
}
}
main();That’s it. That’s an agent loop. Trigger (you run the file), context + decide (the model reads its prompt and the conversation), act (it calls tools), check (results go back in), repeat (the while loop) until done. Everything else in this course is a more sophisticated version of these 35 lines. Stare at it until it’s obvious. It will pay off in every later module.
Step 3 — Give it hands (tools.ts)
A model with no tools can only talk. Tools are what make it an agent. Here’s tools.ts with two tools — one to read a source, one to send the result:
import Anthropic from '@anthropic-ai/sdk';
import { readFileSync } from 'fs';
export const tools: Anthropic.Tool[] = [
{
name: 'read_source',
description: "Read today's raw input the agent should summarize. Returns plain text.",
input_schema: { type: 'object', properties: {}, required: [] },
},
{
name: 'send_digest',
description: 'Send the finished digest to the operator. Call this exactly once when done.',
input_schema: {
type: 'object',
properties: {
subject: { type: 'string', description: 'Short subject line' },
body: { type: 'string', description: 'The digest, in plain text' },
},
required: ['subject', 'body'],
},
},
];
export async function runTool(name: string, input: any): Promise<string> {
switch (name) {
case 'read_source':
// Swap this for your real source: a calendar API, an RSS feed, a DB query.
// To start, just read a local file you drop notes into.
return readFileSync('./source.txt', 'utf-8');
case 'send_digest':
// Start by printing. Swap for real email/Slack once it works on screen.
console.log(`\n=== ${input.subject} ===\n${input.body}\n`);
// Real version: await sendSlack(input.subject, input.body);
return 'Digest sent successfully.';
default:
return `Unknown tool: ${name}`;
}
}Notice the pattern I’m pushing you toward: start with the action faked (a console.log), get the loop working end-to-end, THEN swap in the real integration. This is the single biggest time-saver for beginners. Don’t debug “is my agent thinking right” and “is my Slack webhook configured” at the same time. Get the brain right against fake hands, then bolt on real hands. I still build every new agent this way.
Step 4 — Write the prompt (prompt.md) — this is the real work
The loop and tools are copy-paste. The prompt is where your agent becomes yours. Here’s a production-shaped one. Read the structure, because Module 05 is entirely about why it’s shaped like this:
You are a daily digest agent for {your name / business}. You run once
every morning, unattended. There is no human watching this run — get it
right the first time or fail loudly.
## Your job
1. Call read_source to get today's raw input.
2. Pull out only what actually matters to the operator. Cut filler.
3. Write a tight digest: 3-6 bullets, plain language, most important first.
4. Call send_digest exactly once with a clear subject and the digest body.
5. Then stop.
## Rules
- If read_source returns nothing useful, send a digest that says exactly
that. Do NOT invent items. An empty day is a valid result.
- Never include anything you can't support from the source. No guessing.
- Lead with the single most important item. The operator may only read line one.
- Plain text. No markdown headers, no preamble, no "Here's your digest."
## Done means
You have called send_digest once and have nothing left to do.Look at what that prompt does that a ChatGPT prompt doesn’t:
- It tells the agent it’s running unattended. That changes the model’s behavior — it stops hedging and asking clarifying questions.
- It defines “done” explicitly. Agents loop. Without a clear stop condition they ramble or re-run tools. “Done means you called send_digest once” is a hard finish line.
- It handles the empty case (“an empty day is a valid result”). This one line prevents the #1 production failure: the agent hallucinating content because it assumes there must be some.
- It forbids invention. “Never include anything you can’t support from the source.” This is your anti-hallucination clause, and you’ll write a version of it into every agent you ever ship.
Step 5 — Run it
Drop a few lines into source.txt (pretend they’re today’s calendar or notes), then:
npx tsx src/run.tsYou should watch the agent call read_source, think, then call send_digest, and print your digest. That’s a complete agent. It triggered, gathered context, decided what mattered, took an action, and stopped.
Run it again with source.txt empty. Watch it correctly say “nothing notable today” instead of inventing events. That’s the difference between a demo and a production agent, and you just built it on try one.
Step 6 — Make it real (the 5-minute swap)
Now swap the fakes for reality, one at a time:
Real source. Replace the read_source body with an actual fetch. Reading a feed:
case 'read_source': {
const res = await fetch('https://your-source.example/feed.json');
const data = await res.json();
return JSON.stringify(data.items?.slice(0, 20) ?? []);
}Real delivery. Replace the send_digest body with a Slack webhook (free, 2 minutes to set up at api.slack.com → Incoming Webhooks):
case 'send_digest': {
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: `*${input.subject}*\n${input.body}` }),
});
return 'Posted to Slack.';
}Run it once more. Now it reads your real source and posts to your real Slack. You have a production agent. The only thing missing is the trigger — making it run at 6am without you — and that’s Module 07. For now, you’re the trigger, and that’s fine. You shipped.
Building it with Claude Code instead
If you’d rather not hand-edit files, this whole thing is faster inside Claude Code or Cowork. Open the repo and just say:
“This is an agent starter. I want it to read my Google Calendar each morning and Slack me a digest of today’s meetings, flagging anything that looks like it needs prep. Wire up the calendar read and the Slack send, keep the loop as-is, and update prompt.md.”
Claude Code will edit tools.ts and prompt.md for you. That’s the operator’s move — you stay at the level of what the agent should do, and let the copilot handle the typing. I build most new agents this way now: I describe the job to Claude Code, it scaffolds, I review and run. But you should hand-build this first one so you understand what it’s writing.
Hands-on lab
Build your agent — the yes/yes task you spec’d in Module 01. Not my digest example. Yours.
Step 1. Clone the starter again into a fresh folder named for your task (event-promo, review-replies, whatever it is).
Step 2. Rewrite prompt.md for your job. Keep the four production clauses: “you run unattended,” an explicit “done means,” an empty-case handler, and a “never invent” rule.
Step 3. Define your real tools in tools.ts — but fake the actions first (console.log). Get the loop working against fakes before you touch any real API.
Step 4. Run it until the brain is right. Feed it weird input on purpose — empty source, malformed data — and make sure it fails loudly instead of hallucinating.
Step 5. Swap one fake for the real thing (start with the read, it’s safer than the write). Run again.
Deliverable: an agent that, when you run one command, reads something real about your business and produces a correct, useful action — even if that action is still “print it to my terminal.” That’s your first agent. In Module 03 we’ll figure out what shape it should be so it survives contact with the real world.