Documentation Index
Fetch the complete documentation index at: https://mintlify.com/HKUDS/nanobot/llms.txt
Use this file to discover all available pages before exploring further.
The Heartbeat system allows your agent to wake up periodically, check for pending tasks, and execute them automatically without manual intervention.
Overview
The heartbeat service reads HEARTBEAT.md in your workspace every 30 minutes (configurable) and uses an LLM to decide whether there are active tasks. If tasks are found, the agent executes them and delivers results to your most recently active chat channel.
Key features:
- Periodic wake-up without manual triggers
- LLM-powered task detection using virtual tool calls
- Automatic result delivery to active channels
- Configurable interval and enable/disable control
Architecture
The heartbeat operates in two phases:
Phase 1: Decision
Reads HEARTBEAT.md and asks the LLM via a virtual tool call whether there are active tasks. This avoids unreliable text parsing and token-based detection.
From nanobot/heartbeat/service.py:85-106:
async def _decide(self, content: str) -> tuple[str, str]:
"""Phase 1: ask LLM to decide skip/run via virtual tool call.
Returns (action, tasks) where action is 'skip' or 'run'.
"""
response = await self.provider.chat(
messages=[
{"role": "system", "content": "You are a heartbeat agent. Call the heartbeat tool to report your decision."},
{"role": "user", "content": (
"Review the following HEARTBEAT.md and decide whether there are active tasks.\n\n"
f"{content}"
)},
],
tools=_HEARTBEAT_TOOL,
model=self.model,
)
if not response.has_tool_calls:
return "skip", ""
args = response.tool_calls[0].arguments
return args.get("action", "skip"), args.get("tasks", "")
Phase 2: Execution
Only triggered when Phase 1 returns run. The on_execute callback runs the task through the full agent loop and returns the result to deliver.
From nanobot/heartbeat/service.py:140-163:
async def _tick(self) -> None:
"""Execute a single heartbeat tick."""
content = self._read_heartbeat_file()
if not content:
logger.debug("Heartbeat: HEARTBEAT.md missing or empty")
return
logger.info("Heartbeat: checking for tasks...")
try:
action, tasks = await self._decide(content)
if action != "run":
logger.info("Heartbeat: OK (nothing to report)")
return
logger.info("Heartbeat: tasks found, executing...")
if self.on_execute:
response = await self.on_execute(tasks)
if response and self.on_notify:
logger.info("Heartbeat: completed, delivering response")
await self.on_notify(response)
except Exception:
logger.exception("Heartbeat execution failed")
Usage
Configuration
The heartbeat service is configured when initializing the HeartbeatService class (nanobot/heartbeat/service.py:53-69):
HeartbeatService(
workspace=Path("~/.nanobot/workspace"),
provider=llm_provider,
model="anthropic/claude-opus-4-5",
on_execute=execute_callback,
on_notify=notify_callback,
interval_s=30 * 60, # 30 minutes
enabled=True,
)
| Parameter | Type | Description |
|---|
workspace | Path | Path to workspace containing HEARTBEAT.md |
provider | LLMProvider | LLM provider for decision making |
model | str | Model to use for heartbeat decisions |
on_execute | Callable | Callback to execute tasks (returns result string) |
on_notify | Callable | Callback to deliver results to channels |
interval_s | int | Wake-up interval in seconds (default: 1800) |
enabled | bool | Enable/disable the service |
Setting Up Tasks
Edit ~/.nanobot/workspace/HEARTBEAT.md (created automatically by nanobot onboard):
## Periodic Tasks
- [ ] Check weather forecast and send a summary
- [ ] Scan inbox for urgent emails
- [ ] Monitor server status and alert if issues found
The agent can also manage this file — ask it to “add a periodic task” and it will update HEARTBEAT.md for you.
Starting the Service
From nanobot/heartbeat/service.py:108-119:
async def start(self) -> None:
"""Start the heartbeat service."""
if not self.enabled:
logger.info("Heartbeat disabled")
return
if self._running:
logger.warning("Heartbeat already running")
return
self._running = True
self._task = asyncio.create_task(self._run_loop())
logger.info("Heartbeat started (every {}s)", self.interval_s)
The service runs automatically when you start the gateway:
Manual Trigger
You can manually trigger a heartbeat check without waiting for the interval:
result = await heartbeat_service.trigger_now()
if result:
print(f"Task executed: {result}")
else:
print("No tasks to execute")
Use Cases
1. Morning Briefing
Create a daily morning briefing with news, weather, and calendar:
## Periodic Tasks
- [ ] Fetch top 3 tech news headlines
- [ ] Check today's weather and forecast
- [ ] List today's calendar events
- [ ] Summarize as morning briefing
2. Server Monitoring
Monitor server health and alert on issues:
## Periodic Tasks
- [ ] Check disk space on production server
- [ ] Verify API response times
- [ ] Check error logs for critical issues
- [ ] Alert me if any issues found
3. Inbox Management
Automate email triage and responses:
## Periodic Tasks
- [ ] Scan inbox for emails marked urgent
- [ ] Draft replies for routine inquiries
- [ ] Summarize action items from recent emails
4. Project Status Updates
Automatic project tracking:
## Periodic Tasks
- [ ] Check GitHub PRs needing review
- [ ] List open issues assigned to me
- [ ] Generate weekly progress summary
The heartbeat decision uses a virtual tool call with this schema (nanobot/heartbeat/service.py:14-37):
_HEARTBEAT_TOOL = [
{
"type": "function",
"function": {
"name": "heartbeat",
"description": "Report heartbeat decision after reviewing tasks.",
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["skip", "run"],
"description": "skip = nothing to do, run = has active tasks",
},
"tasks": {
"type": "string",
"description": "Natural-language summary of active tasks (required for run)",
},
},
"required": ["action"],
},
},
}
]
Best Practices
- Keep tasks actionable: Write clear, specific tasks that the agent can execute independently
- Use checkboxes: The
- [ ] format helps the LLM identify incomplete tasks
- Set realistic intervals: Default 30 minutes is good for most use cases; adjust based on task urgency
- Test manually first: Use
trigger_now() or interact directly before relying on automatic execution
- Monitor logs: Check that tasks are being detected and executed as expected
Stopping the Service
To stop the heartbeat service:
Or disable it in the configuration:
HeartbeatService(
# ... other params ...
enabled=False,
)
- Cron Jobs - Schedule tasks with precise timing
- Subagents - Execute background tasks without blocking