Heartbeat Guide

August 12, 2026 · View on GitHub

← Back to README

The Heartbeat wakes the agent on a fixed interval to check for pending work without waiting for user input. Unlike the Schedule tool - which lets the LLM create user-driven cron jobs that deliver output to a channel - the heartbeat is a global self-driven tick the operator configures once.

It exists for autonomy use cases: "every hour, look at my todos and make progress on whatever is next." The agent decides what to do at each tick using a tailored system prompt.

How it works

┌───────────────────────────────────────────────────────────────┐
│  infer daemon (long-running)                 │
│                                                               │
│   ├─ ChannelManagerService    (channels - optional)           │
│   ├─ SchedulerService         (cron jobs - optional)          │
│   └─ HeartbeatService                                         │
│        ├─ time.Ticker(interval)                               │
│        └─ on tick: spawn `infer headless --heartbeat             │
│                                  --session-id <uuid> <prompt>`│
│                    log stdout                                 │
└───────────────────────────────────────────────────────────────┘

                                       │ reads on startup
                          ┌────────────┴────────────────┐
                          │ .infer/heartbeat.yaml       │
                          │ .infer/prompts.yaml         │
                          │   (system_prompt_heartbeat) │
                          └─────────────────────────────┘

Key properties:

  • Off by default. Heartbeat is opt-in via the enabled flag in heartbeat.yaml.
  • Single global instance. One interval, one prompt - for multi-job use cases use the Schedule tool instead.
  • Daemon-bound. Heartbeat only fires while infer daemon is running. If the daemon is down, ticks are skipped (no replay).
  • Fresh session per tick. Each tick gets a new UUID session ID; no context carries between fires. The agent inspects persistent state (todos, scheduled jobs, conversation history) to decide what to do.
  • Overlap-safe. If a tick takes longer than the interval, the next tick is skipped rather than running concurrently.
  • Output to logs. The agent's stdout is logged. Whatever externally-visible action you want the agent to take (post to Telegram, open a PR, run a job) it does via its own tools - the heartbeat is just the trigger.

Setup

1. Generate the config files

infer init

This creates .infer/heartbeat.yaml (disabled by default) and includes a system_prompt_heartbeat entry in .infer/prompts.yaml.

2. Enable and tune the heartbeat

Edit .infer/heartbeat.yaml:

---
enabled: true
interval: 1h            # how often to wake (Go duration: 30s, 5m, 1h, 24h)
initial_delay: 1m       # delay before first tick (avoids fire-on-start)
model: ""               # optional override; empty = agent.model from config.yaml
prompt: "Heartbeat tick - check for any pending tasks, todos, or background work and act on them."

The prompt field is the user message sent to the agent each tick. The system prompt (the steering instructions) lives in .infer/prompts.yaml:

agent:
  system_prompt_heartbeat: |
    You are an autonomous agent that has just been woken up by a
    periodic heartbeat tick.
    ...

The default heartbeat system prompt is conservative - it tells the agent to check pending todos and background tasks, take at most one concrete step per tick, and exit. Override it to fit your workflow.

3. Run the daemon

infer daemon

The daemon hosts up to three subsystems - channels, scheduler, and heartbeat - and starts whichever are enabled. You can run with heartbeat alone (no channels, no scheduler) if that is all you need.

INFO Starting daemon
INFO Heartbeat service started  interval=1h0m0s  initial_delay=1m0s
INFO Daemon ready. Press Ctrl+C to stop.

When a tick fires:

INFO Heartbeat tick - spawning agent  session_id=…  model=
INFO Heartbeat agent output  session_id=…  line={"role":"assistant","content":"…"}
INFO Heartbeat tick complete  session_id=…

Configuration reference

.infer/heartbeat.yaml

FieldTypeDefaultDescription
enabledboolfalseFeature flag. Heartbeat is opt-in.
intervalduration string"1h"How often to fire. Parsed via time.ParseDuration (e.g. 30s, 5m, 1h, 24h).
initial_delayduration string"1m"Delay before the first tick after the daemon starts. Set to "0s" to fire immediately.
modelstring""Optional model override. Empty falls back to agent.model in config.yaml.
promptstring(built-in)The user message sent to the agent each tick.

Environment variables

Mirroring the file fields. Env vars win over heartbeat.yaml.

VariableMaps to
INFER_HEARTBEAT_ENABLEDenabled
INFER_HEARTBEAT_INTERVALinterval
INFER_HEARTBEAT_INITIAL_DELAYinitial_delay
INFER_HEARTBEAT_MODELmodel
INFER_HEARTBEAT_PROMPTprompt
INFER_PROMPTS_AGENT_SYSTEM_PROMPT_HEARTBEATthe system prompt in prompts.yaml

Common patterns

Hourly TODO sweep

# heartbeat.yaml
enabled: true
interval: 1h
prompt: "Sweep open todos. If any are stale (>24h with no progress), pick the highest-priority one and take the next concrete step."

Build / CI watchdog

# heartbeat.yaml
enabled: true
interval: 15m
prompt: "Check the status of the latest GitHub Actions run on this repo. If it failed, summarise the failure in the conversation log."

The agent inspects GitHub state with the gh CLI via the Bash tool - there is no built-in GitHub tool. Read-only gh subcommands (gh run view, gh pr list, ...) are on the standard bash allow-list; keep the prompt scoped to reporting and warn against opening issues or pushing changes automatically.

Troubleshooting

Heartbeat never fires - confirm enabled: true and that infer daemon is running. The daemon logs Heartbeat service started on boot when it picks up the config.

Heartbeat fires too often / not enough - check interval parses correctly. Bad durations (1H, 30 minutes) cause the daemon to fail-fast on startup with a clear error.

Agent does nothing - heartbeat works; the agent decided no action was needed. Check the system prompt - the default explicitly tells it to no-op when nothing is pending.

"Heartbeat tick skipped - previous run still in flight" - your agent is taking longer than interval to complete. Either increase the interval, simplify the prompt, or set a tighter agent.max_turns in config.yaml.

Comparison with the Schedule tool

HeartbeatSchedule tool
Configured byOperator (yaml file)LLM (via tool calls)
MultiplicitySingle global tickMany user-defined jobs
TriggerFixed interval (Go duration)Cron expression (per job)
OutputLogsA messaging channel (Telegram, …)
Use caseAutonomous self-monitoring"Remind me at 8am tomorrow"

The two are complementary and can run side by side in the same daemon.