What is heartbeat-state.json?
Most people using OpenClaw heartbeats — as configured in HEARTBEAT.md — never look at memory/heartbeat-state.json. They only notice it when a check stops firing, a timestamp looks stale, or they want to know whether the last run actually wrote state. The file is small, but it does an important job: it keeps a record of when each heartbeat check last ran.
Think of it as the heartbeat’s notebook. If the notebook says a check ran five minutes ago, that is a very different story from a notebook that has not changed all day. On this workspace, the file is intentionally lean and may contain only a lastChecks object. That is enough to answer the first debugging question: did the heartbeat run, and when?
If you publish or document this workflow in WordPress, this file is one of those quiet little details that makes the system easier to trust.
Where to find it
The file lives at memory/heartbeat-state.json inside the workspace. It sits next to the other memory files that OpenClaw uses to keep track of recent activity.
On this machine, a current example looks like this:
{
"lastChecks": {
"memory_maintenance": 1778195580
}
}
That may look tiny, but it is enough. The key tells you which check ran. The number tells you when it last happened. For a heartbeat system, that is the main truth you care about. You do not need a giant database just to answer a simple question like, “Did this task run recently?” A tiny JSON file is faster to inspect, easier to reset, and much less fragile when you are debugging from the command line.
When the heartbeat grows, the file can grow with it. One check might handle memory maintenance. Another might watch for external updates. Each one can get its own entry under lastChecks, which keeps the file readable even when there are several periodic jobs in play.
What each field means
In the current setup, the structure is simple. The main thing to understand is that the file records the last known run time for each check.
| Field | Meaning | Why it matters |
|---|---|---|
lastChecks |
An object that maps each heartbeat check name to its last run time. | This is the core state the heartbeat reads and updates. |
memory_maintenance |
The name of the specific check being tracked. | Lets you see whether the memory upkeep job actually ran. |
1778195580 |
A Unix timestamp in seconds. | Tells you the exact last run time, regardless of timezone display. |
If you are wondering why the file uses a Unix timestamp instead of a human date, the answer is consistency. Machines compare numbers easily. Humans can convert the number when they need to. On macOS, date -r 1778195580 turns it into a readable time. On Linux, use date -d @1778195580.
Full working example
This is a realistic example of a healthy state file. It is copy-paste ready for testing or reference:
{
"lastChecks": {
"memory_maintenance": 1778195580,
"heartbeat_monitor": 1778195880,
"news_scan": 1778196180
}
}
In that example, each key is a different check. The timestamps are intentionally not identical, because each task may run on a different cadence.
How to read it to check if your heartbeat ran
The fastest way to read this file is to compare the stored timestamp with the time you expect the last run to have happened.
- If the timestamp is recent, the heartbeat probably ran.
- If the timestamp is stale, the heartbeat likely did not complete.
- If the key is missing, the check may have never run or the state write failed.
The important detail is this: the file usually tells you about the last successful state write, not every internal failure. If a check crashes before it saves state, you may only see an old timestamp. That is still useful. It tells you the last known good run and makes the problem visible fast.
This is also why the file is helpful after a rename. If you change a check name from news_scan to openclaw_news, the old key will not magically update itself. Seeing the old key in state tells you the workflow changed and the state file did not get reset yet. That kind of mismatch is one of the fastest ways to spot a config drift problem.
A practical debugging flow looks like this:
- Open
memory/heartbeat-state.json. - Find the check name you care about.
- Convert the timestamp if needed.
- Compare it with the schedule.
- If it is old, check logs and the heartbeat workflow itself.
How to reset or manually edit it (and when you should)
Sometimes the cleanest fix is to reset the file. That is usually safe when you have changed the heartbeat logic, renamed a check, or want to clear out stale state after a major workflow change.
A minimal reset looks like this:
{
"lastChecks": {}
}
You should not edit it casually during active troubleshooting unless you know what you are trying to prove. Manual edits are best for one of three cases:
- You renamed a check and want to clear the old key.
- You are testing the scheduler and need a clean baseline.
- The file was corrupted and needs to be rebuilt.
If you do edit it, keep the JSON valid, keep the structure simple, and do not invent timestamps unless you have a reason. The point of the file is truth, not decoration.
Common issues the file reveals
This little file can expose a surprising amount of trouble. Most of the time, the problem is not the JSON itself. The problem is what the JSON failed to record.
- Stale timestamp – the check stopped updating, so the heartbeat may be stuck or disabled.
- Missing key – the check was renamed, never run, or failed before the state write.
- Bad timestamp – the value is not a valid Unix time, which usually means manual editing went wrong.
- Unexpected reset – the file was cleared, which can happen after a deploy or cleanup task.
Example of a skipped or failed run
Because this state file usually records the last successful write, a failed or skipped run often shows up as an old timestamp instead of a special error flag. That means the file itself can still diagnose the problem even when it does not spell out the failure.
{
"lastChecks": {
"memory_maintenance": 1778195580,
"heartbeat_monitor": 1778195880
}
}
In this example, the scheduler was expected to update heartbeat_monitor again later, but the number never advanced. That is the clue. When you see this, check the run logs, look for a timeout, and confirm the job is still enabled.
FAQ
Is heartbeat-state.json required?
No, but it is very useful. Without it, the heartbeat has less memory of what ran last, so debugging becomes harder.
Can I delete it?
You can, but only if you are prepared for the heartbeat to recreate it or start from a blank state. Deleting it is basically a reset.
Why does it use timestamps instead of dates?
Because timestamps are easier for code to compare and update. Humans can convert them when needed.
What should I check first if the heartbeat looks broken?
Check the timestamp in lastChecks, then check the heartbeat logs, then verify the schedule. In most cases, the file tells you whether you are dealing with a missing run, a stale run, or a clean reset.
If you want the short version, here it is: heartbeat-state.json is the heartbeat’s memory. When it changes, something ran. When it does not, that is your first clue.
Not Ready to Self-Host? Try ClawTrust
Building agents locally is powerful, but managing infrastructure, security, and scaling takes effort. ClawTrust provides managed OpenClaw hosting — your agents run securely in the cloud with automatic scaling, no DevOps overhead, and 24/7 monitoring.
This post contains affiliate links. If you buy through them, I may earn a small commission at no extra cost to you.


Quick question, does the heartbeat-state.json get created automatically on first run or do you need to seed it manually with an empty object?
Great question. For heartbeat state json Explained Tracks, I would start with the simplest workable setup and only add complexity if you actually need it. That keeps things easier to troubleshoot and usually gets you to a better result faster.
The line about
lastChecksbeing enough for first-pass debugging is useful. I often overcomplicate state files, then spend more time parsing them than fixing the heartbeat.That was the intention. Start with the smallest state that answers the question, “did it run, and when?” If debugging later needs more context, you can add a second file or log line without bloating the core heartbeat state.
Appreciated the reminder that the file is intentionally lean. Do you think it is worth adding history later, or is a tiny state file usually the right call unless you have a real audit need?
Usually the tiny file wins unless you have a real compliance or audit reason to keep history. Extra detail is only useful if someone actually reads it during a failure, otherwise it just adds noise.
This cleared up why a stale timestamp matters so much. If the notebook says a check ran five minutes ago, that is instantly more actionable than just knowing the job exists.
Exactly, that is why the timestamp is so important. It turns a vague suspicion into a concrete debugging step, which is usually the fastest way to separate a scheduling issue from a task issue.