Running an AI Coding Bot on Raspberry Pi — Part 5 Heartbeats, Notifications & GitHub Project Workflow
Previous: Part 4 - Codex Delegation
After getting Codex running in tmux and writing code reliably, I hit a new problem: silence. Codex would finish its work and sit there, while I had no idea whether it succeeded, failed, or was still running. This post covers closing the loop — wake triggers, the heartbeat system, and how I eventually got to a workflow where pointing the bot at a GitHub project spawns independent agents for each issue automatically.
The silence problem
The flow I had: OpenClaw spawns Codex via spawn-agent.sh, Codex works, Codex writes results to outbox.json. But OpenClaw wasn't watching outbox.json. It spawned the task and forgot about it.
The fix has two parts:
- Codex sends a wake signal to OpenClaw when it finishes
- OpenClaw's heartbeat picks up that signal and checks the outbox
The WAKE_TRIGGER
At the end of every Codex prompt, I append:
WAKE_TRIGGER="When completely finished, write the outbox to \$CODEX_MAILBOX/outbox.json with status done, then run this command to notify OpenClaw immediately:
openclaw system event --text \"Task $TASK_ID completed\" --mode now"openclaw system event --mode now wakes up the heartbeat immediately instead of waiting for the next scheduled tick.
check-agents.sh
This script is what runs when the heartbeat fires. It:
- Reads
active-tasks.jsonand taskoutbox.jsonfiles - Finds tasks signaled as
doneby Codex but not yet updated inactive-tasks.json - Checks the PRs from
outbox.jsonto see if CI/CD passed - Sends a Discord notification
- Updates
active-tasks.json
The Discord notification uses the first-class CLI:
notify() {
local msg="$1"
openclaw message send \
--channel discord \
--target "channel:$CHANNEL" \
--message "$msg" \
2>/dev/null || echo "$LOG_PREFIX WARN: discord notify failed for $TASK_ID"
}HEARTBEAT.md — the task registry
The heartbeat system in OpenClaw lets you define named tasks that run on a schedule. The config in openclaw.json:
{
"agents": {
"defaults": {
"heartbeat": {
"every": "5s",
"target": "last",
"directPolicy": "allow",
"lightContext": true,
"isolatedSession": true,
"activeHours": {
"start": "08:00",
"end": "23:00",
"timezone": "Europe/Lisbon"
}
}
}
}
}every: "5s" is how often the gateway wakes OpenClaw to run a heartbeat turn at all. This is the outer tick. Inside HEARTBEAT.md, each named task has its own interval — that task only fires if enough time has passed since it last ran.
The flow:
every 5s → gateway wakes OpenClaw → reads HEARTBEAT.md →
checks if check-active-tasks is due (every 20s) →
if due: runs it | if not due: HEARTBEAT_OKThe HEARTBEAT.md task for agent monitoring:
tasks:
- name: on-agent-complete
interval: 5s
prompt: |
Check if any system events arrived since last heartbeat.
If so, run bash ~/.openclaw/workspace/scripts/check-agents.sh immediately
and report results to Viktor in Discord.
Otherwise reply HEARTBEAT_OK.To check if heartbeat is running:
openclaw logs 2>/dev/null | grep -i heartbeat | tail -20To force a heartbeat run for debugging:
openclaw system event --text "heartbeat test (force run)" --mode now --jsonThen immediately check what it did:
openclaw system heartbeat last --jsonHardcoding Discord channel IDs in TOOLS.md
Rather than asking me for a channel ID during every heartbeat run, I put them in TOOLS.md:
| Heartbeat name | Discord Channel ID | Purpose |
|---------------------------|-------------------------------|------------------------------------|
| project-autopick | 1493601197670666412 | Autopick spawn notifications |
| roadmap-context-to-issues | 1493558359495016488 | New issues creation notifications |And in HEARTBEAT.md, a global instruction:
All tasks listed here are autonomous heartbeat tasks.
When a task is invoked from this registry, run it in heartbeat/autonomous mode.
Do not ask Viktor for clarification unless the task explicitly says to.
Do not ask Viktor for a channel ID during heartbeat runs.
Use the heartbeat channel registry in TOOLS.md when a channel must be resolved.
If nothing needs attention, reply HEARTBEAT_OK.The GitHub project workflow
Once the basic notification loop was working, I gave the bot access to my GitHub project:
gh auth refresh -s read:projectThen manually tested asking it to handle issues from a GitHub project URL. The result was wild — it spawned two agents to handle two issues independently:
✅ PR #21 ready for review — issue-19-single-wizard
Completed docs-first plan and single Ink wizard UI with arrow navigation; pushed branch and opened PR #21 linked to issue #19.
https://github.com/IaC-Toolbox/iac-toolbox-cli/pull/21
✅ PR #22 ready for review — issue-18-pnpm
Migrated iac-toolbox-cli to pnpm, validated, committed, pushed, and opened PR #22 linked to issue #18.
https://github.com/IaC-Toolbox/iac-toolbox-cli/pull/22Two parallel agents, two PRs, zero manual coding. That's the payoff for all the plumbing work.
The project-autopick heartbeat
To make this happen autonomously without me prompting it, I added a heartbeat task that reads the GitHub project and picks up unassigned issues:
- name: project-autopick
interval: 60m
prompt_file: ~/.openclaw/workspace/heartbeat-prompts/project-autopick.mdThe prompt file instructs the bot to scan for open issues with no assignee, check active-tasks.json to avoid duplicating work, and spawn agents for anything actionable.
Handling merge conflicts from parallel PRs
Running parallel agents creates a new problem: merge conflicts. Two levels:
- Conflicts before merging — solvable by rebasing the branch and asking the agent to resolve
- Conflicts after merging — when one PR merges, the other branch is now stale
The second one is the harder problem. GitHub webhooks could potentially trigger rebases automatically, but for now I handle it manually: after merging a PR, I ask OpenClaw to rebase the other open branches.
Cleaning stale sessions
One issue I hit repeatedly: old OpenClaw session files contributing stale context to new runs, causing the agent to replay old errors as if they were current. The fix:
mkdir -p ~/.openclaw/agents/main/sessions-archive
mv ~/.openclaw/agents/main/sessions/*.jsonl ~/.openclaw/agents/main/sessions-archive/If your bot keeps giving you the same stale error after you've already fixed the underlying problem, session cleanup is the first thing to try.
What's working now
- Codex sends a wake trigger on completion
- Heartbeat picks it up within seconds
- Discord notification with PR link fires automatically
- GitHub project scanning picks up unassigned issues
- Parallel agents work independently with git worktrees
- Channel IDs are stable in config — no runtime asking
The next post covers the final evolution: giving the bot a proper memory system and making it generate its own work from a product roadmap you write.
Previous: Part 4 - Codex Delegation | Next: Part 6 - Obsidian & Autonomous Loop