The question keeps popping up in GitHub issues and on our Discord: “Can OpenClaw read my WHOOP data and summarize it every morning?” Short answer: yes. Longer answer: you’ll need a WHOOP developer token, a few lines of TypeScript, and about 20 minutes of config work. This guide walks through the whole flow—raw API pull, OpenClaw skill wiring, daily briefings, trend analysis, and a more advanced trick where the agent alters its recommendations when your biomarkers go red.
Why bother wiring WHOOP into OpenClaw?
OpenClaw already talks to Slack, iMessage and fifteen other channels. WHOOP fills the gap on physiological context. Sleep debt, recovery score and HRV trend are numbers we can feed into the agent so it can:
- Ping your stand-up channel with “go easy, recovery 28%” instead of the usual task list.
- Triage calendar invites if strain is high.
- Surface long-term HRV decline as a health risk in a weekly digest.
A handful of users in the community (#integrations) report cutting down decision fatigue because the agent filters commitments based on readiness score. Worth the 200 lines of glue code IMO.
Prerequisites and versions that actually work
Keep your versions straight; most bug reports trace back to mismatched deps.
- OpenClaw gateway ≥ 0.47.2
- OpenClaw daemon ≥ 0.26.0
- Node.js 22+ (WHOOP SDK requires fetch() without flags)
- WHOOP Developer Account (free, request at developer.whoop.com)
- A persistent store—PostgreSQL 15 or the built-in SQLite is fine for a single user
Generate a WHOOP Personal Access Token (PAT) after approval. Write it to WHOOP_TOKEN in your agent’s .env.
Step 1 – Install the WHOOP skill skeleton
OpenClaw doesn’t ship an official WHOOP plugin yet, so we scaffold a custom skill.
# in your agent repo
mkdir -p skills/whoop && cd skills/whoop
npm init -y
npm install openclaw-sdk@0.47.2 node-fetch@4.0.0 dayjs@1.11.10
Create index.ts:
import fetch from 'node-fetch';
import dayjs from 'dayjs';
import { defineSkill } from 'openclaw-sdk';
const WHOOP_TOKEN = process.env.WHOOP_TOKEN;
const BASE = 'https://api.prod.whoop.com';
async function getJson(url: string) {
const res = await fetch(url, {
headers: {
'Authorization': `Bearer ${WHOOP_TOKEN}`
}
});
if (!res.ok) throw new Error(`WHOOP ${res.status}`);
return res.json();
}
export default defineSkill({
name: 'whoop.today',
description: 'Pull recovery, sleep, strain for the last 24h',
async run(ctx) {
const today = dayjs().startOf('day').toISOString();
const url = `${BASE}/metrics?start=${today}&end=${dayjs().toISOString()}`;
const data = await getJson(url);
return ctx.reply(JSON.stringify(data));
}
});
Register the skill in agent.yml:
skills:
- ./skills/whoop
Restart the daemon:
openclaw daemon restart
Early testers hit 401s because the WHOOP PAT expires every 180 days. Put a calendar reminder; the API won’t warn you.
Step 2 – Fetch recovery, strain and sleep data reliably
WHOOP API endpoints worth your time
- /v1/activity – day-level strain
- /v1/recovery – recovery score, HRV, RHR
- /v1/sleep – hours in bed, sleep debt, disturbances
WHOOP rate-limits at 120 requests/min and 10 reqs/second bursts. We batch to stay safe.
// skills/whoop/fetch.ts
export async function fetchDay(date: string) {
const [recovery, strain, sleep] = await Promise.all([
getJson(`${BASE}/v1/recovery/${date}`),
getJson(`${BASE}/v1/activity/${date}`),
getJson(`${BASE}/v1/sleep/${date}`)
]);
return { date, recovery, strain, sleep };
}
Community note: the start= query takes local ISO dates, but the API returns UTC timestamps. Daylight savings made my recovery show under the wrong day until I forced tz=auto on the gateway side.
Caching the payload
Saving every call means nicer latency for the skill and cheaper WHOOP quota.
import { kv } from 'openclaw-sdk';
async function cachedDay(date: string) {
const key = `whoop:${date}`;
const hit = await kv.get(key);
if (hit) return JSON.parse(hit);
const fresh = await fetchDay(date);
await kv.put(key, JSON.stringify(fresh), { ttl: 86400 });
return fresh;
}
Step 3 – Building the daily health briefing
OpenClaw scheduling syntax is cron-ish. Add a job to fire at 07:00 local:
# agent.yml
cron:
- schedule: "0 7 * * *"
call: whoop.briefing
Implement the skill:
export default defineSkill({
name: 'whoop.briefing',
description: 'Summarize last night recovery and today recommendations',
async run(ctx) {
const today = dayjs().format('YYYY-MM-DD');
const data = await cachedDay(today);
const { recovery, strain, sleep } = data;
const txt = `Recovery: ${recovery.score}% (HRV ${recovery.hrv}ms)\n` +
`Sleep: ${sleep.hours}h (${sleep.debt}h debt)\n` +
`Yesterday strain: ${strain.score}\n` +
recommend(recovery.score, sleep.debt);
await ctx.reply(txt, { channel: 'slack://#health' });
}
});
function recommend(recov: number, debt: number) {
if (recov < 33) return 'Take a rest day. Reschedule high-intensity tasks.';
if (debt > 2) return 'Prioritize sleep tonight; schedule ends at 22:00.';
return 'Green light. Proceed with normal workload.';
}
Run openclaw skills test whoop.briefing to see sample output. My own Slack looks like:
Recovery: 64% (HRV 77ms)
Sleep: 7.3h (0.4h debt)
Yesterday strain: 15.1
Green light. Proceed with normal workload.
Engineers on HN complained about verbose Slack bots. Keep it < 5 lines or it gets muted.
Step 4 – Trend analysis & weekly summaries
Daily numbers are noisy. We add a 7-day rolling average graph and push it every Monday.
Collecting a week of datapoints
async function weekDates() {
return Array.from({ length: 7 }).map((_, i) =>
dayjs().subtract(i, 'day').format('YYYY-MM-DD')
);
}
export async function fetchWeek() {
const days = await Promise.all((await weekDates()).map(cachedDay));
return days.reverse(); // chronological
}
Render simple ASCII chart (cheating is fine)
import asciichart from 'asciichart';
function renderChart(points: number[]) {
return '```\n' + asciichart.plot(points, { height: 4 }) + '\n```';
}
Send on Monday 07:02:
# agent.yml
cron:
- schedule: "2 7 * * 1"
call: whoop.weekly
Skill:
export default defineSkill({
name: 'whoop.weekly',
description: 'Weekly HRV & recovery overview',
async run(ctx) {
const week = await fetchWeek();
const recovery = week.map(d => d.recovery.score);
const hrv = week.map(d => d.recovery.hrv);
const body = [
'7-day Recovery %',
renderChart(recovery),
'HRV trend',
renderChart(hrv)
].join('\n');
await ctx.reply(body, { channel: 'slack://#health' });
}
});
Is the ASCII ugly? A bit, but it’s enough signal. If you want pretties, pipe to QuickChart API and embed an image.
Step 5 – Adaptive recommendations using biomarker triggers
This is where most readers bail, but it’s the fun bit. We hook biomarker thresholds to OpenClaw’s decision engine so your agent changes behaviour when you’re fried.
Expose biomarkers as agent memory
export async function syncBiomarkers() {
const today = dayjs().format('YYYY-MM-DD');
const { recovery } = await cachedDay(today);
await memory.set('biomarker.recoveryScore', recovery.score);
await memory.set('biomarker.hrv', recovery.hrv);
}
Schedule it hourly—cheap call thanks to cache.
Use memory in other skills
export default defineSkill({
name: 'calendar.assist',
description: 'Accept/decline invites based on workload & recovery',
async run(ctx) {
const recov = await memory.get('biomarker.recoveryScore');
const invites = await ctx.call('composio.googleCalendar.listInvites');
const decisions = invites.map(e => decide(e, recov));
await ctx.reply(format(decisions));
}
});
function decide(event, recov) {
if (recov < 40 && event.type === 'optional') return 'decline';
if (recov < 30 && event.type === 'mandatory' && event.length>60) return 'reschedule';
return 'accept';
}
Now your agent turns down 90-minute brainstorming sessions when your HRV tanks. My team’s Slack log shows a 12% meeting drop week-over-week since enabling this.
Hard edges, rate limits, privacy and other gotchas
- Token rotation: WHOOP PAT cannot be refreshed programmatically. Manual copy-paste every six months. If you forget, API returns 401 and the skill crashes. We added
if (err.status===401) ctx.notify('WHOOP token expired')in prod. - Latency: WHOOP endpoints are often 800-900 ms from EU-west. Cache aggressively.
- Data gaps: WHOOP sometimes backfills metrics after firmware updates. We run a nightly job to re-fetch the last 3 days.
- Privacy: Health data is sensitive. If you deploy on ClawCloud, enable project-level secrets and restrict skill logs (
logs: false) so raw HRV doesn’t land in CloudWatch. - Multi-user: WHOOP tokens are user-scoped. For teams, spin one agent per athlete; the API doesn’t support service accounts.
What to try next
The basics work: you get a morning recovery brief and the calendar skill reacts to low readiness. Next steps the community is hacking on:
- Pipe WHOOP strain into Notion via Composio → auto-tagging “heavy day” journal entries.
- Correlate HRV vs GitHub PR throughput in BigQuery. Early numbers show a mild connection.
- Trigger Oura ring import too and reconcile overlapping metrics (good luck).
- Build a “sleep debt payback” leaderboard for the engineering org—gamification but with concrete physiology.
OpenClaw thrives on weird integrations. If you extend this, drop a PR or share in #whoop-integration. Somebody will test it within hours.
Done reading? Go fetch your PAT and wire the skill; your agent will tell you when to call it a night.