Most productivity systems fail because they require you to show up.
You have to check the app. Review the list. Decide what's next. Every decision is friction, and friction compounds. Skip one day and you're catching up. Skip two and you're rebuilding the habit.
The heartbeat is different. It doesn't wait for you. Every 30 minutes, it runs. Gathers state. Decides. Acts. Whether you're paying attention or not.
This post is about how that works—and why deterministic rules beat constant LLM calls for routine decisions.
The Gather-Decide-Act Cycle
Every heartbeat cycle has three phases:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Gather │ ──▶ │ Decide │ ──▶ │ Act │
│ State │ │ (Rules) │ │ (Prompt) │
└─────────────┘ └─────────────┘ └─────────────┘
Gather pulls data from everywhere: How many tasks are open? Any uncommitted code? Emails unread? Meetings soon? CI status?
tasks_open=$(count_tasks "$WORKSPACE/tasks/open")
tasks_doing=$(count_tasks "$WORKSPACE/tasks/doing")
git_dirty=$(git -C "$REPO" status --porcelain | wc -l)
ci_status=$(gh run list --limit 1 --json conclusion -q '.[0].conclusion')Decide applies a priority ladder. Fixed rules, strict order. First match wins.
if ci_status == "failure":
return "fix_ci"
if tasks_doing > 0 and not blocked:
return "continue_task"
if tasks_open > 0 and tasks_doing < 3:
return "pickup_task"
# ...Act emits a prompt. Not a suggestion—a directive. "Continue working on task X" or "CI is red, fix the build."
The cycle completes. State is logged. Cooldowns update. Then the system waits for the next heartbeat.
Why Rules Beat LLM Calls
Here's the tempting alternative: just ask an LLM every cycle. "Given this state, what should I do next?"
I tried it. It doesn't work well. Here's why:
1. LLMs are expensive for routine decisions.
A heartbeat runs every 30 minutes. That's 48 times per day. At even $0.01 per call, you're paying $15/month just to decide what to do next. And that's before the actual work.
A rule evaluation costs nothing. The Python function runs in milliseconds. No API call, no latency, no rate limits.
2. LLMs are inconsistent.
Ask an LLM the same question twice, you might get different answers. Sometimes it prioritizes email. Sometimes it says continue the active task. The variance is a feature for creative work—it's a bug for productivity.
Rules are deterministic. Same state, same output. Every time. You can predict what the system will do, which means you can trust it.
3. LLMs require context that's expensive to maintain.
To make good decisions, an LLM needs context: your priorities, your current projects, your constraints. That context has to be packed into every prompt. Token costs scale with context size.
Rules encode context once. "Email is lower priority than active work" is a one-time decision baked into the priority ladder. No tokens needed per invocation.
4. LLMs can be reasoned with—rules can't.
This sounds like a downside. It's actually the point.
I don't want to negotiate with my productivity system. I don't want it to convince me that "actually, you should check Twitter." The rules are authoritative. When the system says "fix CI," I fix CI. No discussion.
When to Use LLMs
Rules handle the what. LLMs handle the how.
The heartbeat decides: "Pick up a new task." An LLM decides: "Here's how to implement that feature."
The heartbeat decides: "Generate tasks to refill the queue." An LLM does the actual generation.
The boundary is routine vs. novel. Routine decisions—what to prioritize, when to check email, whether to continue or switch—those are rule territory. Novel decisions—how to solve this bug, what to write in this post—that's LLM territory.
This division is efficient. Cheap rules handle high-frequency decisions. Expensive LLMs handle low-frequency, high-value work. The system optimizes for both cost and quality.
Cooldowns and State Tracking
Without rate limiting, the system would thrash. Check email, do one thing, check email again. The cooldown mechanism prevents this:
COOLDOWNS = {
"email": 30, # minutes
"slack": 15,
"calendar": 60,
"generate": 240, # 4 hours
}State persists in a JSON file:
{
"lastChecks": {
"email": 1710723600,
"slack": null,
"calendar": 1710720000
},
"lastAction": {
"timestamp": 1710724500,
"action_id": "continue_task",
"task": "blog-post-heartbeat"
}
}When the heartbeat evaluates "should I check email?", it computes: now - lastChecks.email > 30 minutes. If not, email is ineligible this cycle. The priority ladder skips it and moves on.
This creates rhythm. Email 4 times a day, not 48. Calendar checks every hour. Task generation when the queue runs low. The system has pacing, not just priority.
The state file is also debugging. When the system makes a surprising decision, I can inspect the state. What were the timestamps? What was the task count? The decision becomes explainable.
Maintaining Momentum Without Intervention
Here's the core productivity insight: the system runs whether or not you're paying attention.
Most productivity tools require activation. Open the app, review the list, choose what's next. The heartbeat inverts this. It activates on its own schedule. You respond to it, not the other way around.
This has psychological effects:
No cold starts. When I sit down to work, there's already a prompt waiting. "Continue task X." I don't have to remember where I was. I don't have to decide what matters. The decision is made.
No decision fatigue. The heartbeat makes the meta-decisions so I can focus on execution. What to work on? Already decided. When to check email? Already decided. Whether to pick up another task? Already decided.
Guilt-free breaks. If I step away for two hours, the heartbeat kept running. It handled the decisions. When I return, there's a clear next action. No catch-up, no review, just execute.
Momentum maintenance. The system never stops. Even when I'm not working, it's tracking state. When I return, the state is current. The decision reflects reality, not yesterday's reality.
The Flywheel Effect
Here's what happens over time:
- Heartbeat fires. "Pick up task X."
- Task completes. State updates: one less open, one more done.
- Heartbeat fires. State shows capacity. "Pick up task Y."
- Tasks deplete. Queue runs low.
- Heartbeat fires. "Generate 10 tasks to refill the queue."
- Tasks generated. Queue refills.
- Heartbeat fires. "Pick up task Z."
The system is self-sustaining. It depletes work, refills work, depletes again. No human intervention required to maintain the cycle.
This is the flywheel. Once spinning, it sustains itself. Energy goes into execution, not coordination. The overhead of "what should I do?" drops to zero.
Implementation Notes
If you want to build something similar:
Start with the state gatherer. What sources matter? Tasks, calendar, git, email? Build the data pipeline first. Clean JSON out.
Keep the decision ladder simple. Five rules is better than fifty. Start with: incidents, active work, new work, cleanup. Add complexity only when needed.
Make cooldowns configurable. You'll tune them. A lot. 30-minute email cooldown might become 45. Or 20. Depends on your workflow.
Log everything. Every decision, every rejection reason, every state snapshot. You'll need it for debugging.
Separate gather from decide. Shell for gathering (orchestrating external tools). Python for deciding (clean logic). The boundary matters.
The Meta Point
This post exists because of the heartbeat.
The system decided I had capacity. It selected "write blog post" from the queue. I executed. The system will log completion, update state, and decide what's next.
I didn't choose to write this post today. The system chose. I just showed up and did the work.
That's the productivity system. Not willpower. Not motivation. Not elaborate planning. Just: gather state, apply rules, execute, repeat.
The heartbeat keeps beating. Work gets done.