I made my AI agent text me first
Every agent waits for you. This one starts the conversation.
My claw could think, remember, talk on WhatsApp, and even reach into the real world with tools. But it had one thing in common with every agent before it: it only ever spoke when spoken to. It waited.
So I gave it a heartbeat. Now, out of nowhere, my phone buzzes:
"💧 Water break! How much did you drink?" I reply "a glass",
it estimates ~250ml, logs it, and tells me 1,250 / 3,000 ml for the day. Same
brain, same memory as the last four episodes — it just learned to start the conversation.
The problem
Every agent in this series so far has been reactive. You text it, it answers. Useful, but passive — it's a thing you poke. A real assistant doesn't just sit there waiting for you to remember it exists. It checks in. It nudges. It starts the conversation when something matters. The claw couldn't do any of that, because nothing in it ever ran on its own.
The code
The whole lesson is one timer. In whatsapp.ts, when the client is ready, we
grab our own chat id and set up an interval that messages it — unprompted.
// whatsapp.ts — a heartbeat: a timer that messages YOU, unprompted.
client.on("ready", () => {
const me = client.info.wid._serialized;
onScheduleReminder((hours) => {
const ms = hours * 60 * 60 * 1000;
setInterval(() => {
client.sendMessage(me, "💧 Water break! How much did you drink?");
}, ms);
return ms;
});
});
That's the heartbeat. The other half lives in claude.ts: a tool that runs when
you reply, estimating millilitres and reporting the running total against a 3,000ml goal.
// claude.ts — when you reply, the agent logs it and cheers you on.
tool("log_water", "Record water the user drank (estimate ml from 'a glass' ~250).",
{ ml: z.number() },
async ({ ml }) => {
addWater(ml);
return text(`logged ${ml}ml — today: ${waterToday()} / 3000ml`);
});
There are two siblings to log_water: set_water_reminder (so you can
turn the heartbeat on just by asking) and water_today (so you can ask "how much
today?" any time). All three read and write a tiny SQLite water table.
How it works
This is the flip. Before today, code only ran in response to a message — a handler
woke up, called ask(), and replied. A heartbeat doesn't wait for anyone:
setInterval fires on its own and client.sendMessage drops a normal
WhatsApp message into your own chat. The agent speaks first.
When you reply — "a glass", "500ml", "done" — that's just an incoming message, so the usual
handler runs ask() as always. The model reads "a glass", decides to call
log_water with ml: 250, the tool adds it to the water
table, and you get back 1,250 / 3,000 ml. You never wrote a parser for "a glass" —
the model did the estimating, the tool did the math.
And one timer can do more than nag you about water. The same heartbeat also pushes me a daily news briefing in the morning — one pulse, many nudges.
One caveat — the timer is in memory
This setInterval lives in the running process. Restart the claw and the schedule
is gone — you'd have to ask it to remind you again. That's fine for a demo, but a real version
persists the schedule: a row in the database it reloads on boot, or a proper
cron. The heartbeat should survive a restart the same way the memory does. Small first, though —
one timer teaches the whole idea.
Try it yourself
- Add the
setIntervalheartbeat towhatsapp.tsand thelog_water/set_water_reminder/water_todaytools toclaude.ts, backed by awatertable. - Run it:
npm run whatsapp, then text yourselfremind me to drink water every 3 hours. - Wait for the nudge, reply with how much you drank, then ask
how much today?
Don't want to wait three hours to see it work? Set WATER_MS=15000 and the
heartbeat fires every few seconds instead — perfect for a demo. Watch it text you first, log
your reply, and walk your total toward 3,000ml.
E06 — Give it a personality
The claw can act and it starts the conversation now — but it still sounds like a generic bot. Next we give it a personality, a voice of its own. (Coming soon.)
Watch the full build: the zepto-claw E05 Short, and subscribe on YouTube for E06. Catching up? Start with E01 · E02 · E03 · E04.