Skip to main content
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,
)
ParameterTypeDescription
workspacePathPath to workspace containing HEARTBEAT.md
providerLLMProviderLLM provider for decision making
modelstrModel to use for heartbeat decisions
on_executeCallableCallback to execute tasks (returns result string)
on_notifyCallableCallback to deliver results to channels
interval_sintWake-up interval in seconds (default: 1800)
enabledboolEnable/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:
nanobot 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

Tool Schema

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

  1. Keep tasks actionable: Write clear, specific tasks that the agent can execute independently
  2. Use checkboxes: The - [ ] format helps the LLM identify incomplete tasks
  3. Set realistic intervals: Default 30 minutes is good for most use cases; adjust based on task urgency
  4. Test manually first: Use trigger_now() or interact directly before relying on automatic execution
  5. Monitor logs: Check that tasks are being detected and executed as expected

Stopping the Service

To stop the heartbeat service:
heartbeat_service.stop()
Or disable it in the configuration:
HeartbeatService(
    # ... other params ...
    enabled=False,
)
  • Cron Jobs - Schedule tasks with precise timing
  • Subagents - Execute background tasks without blocking

Build docs developers (and LLMs) love