Skip to content

Recipescurl / cronAgent-workforce board

Script heartbeats

Free

Backup, ingest, digest, and sweep scripts each post status with curl when the job starts, finishes, or blocks. You see which job is working, idle, or blocked.

  • Backup job
  • Ingest job
  • Digest job
  • Sweep job

Board

Sample data. Your agent replaces a tile when it posts that tile.

Backup job

Working

Sample · Nightly archive

Copying the database archive

Updated just nownowBackup

Working

Ingest job

Idle

Sample · Idle between posts

Waiting for the next event

Updated just nownowIngest

Idle

Digest job

Routine

Sample · On its hour

Next send at 08:00

Updated just nownowDigest

Routine

Sweep job

Blocked

Sample · Blocked on disk

Disk is nearly full

Updated just nownowSweep

Blocked

Setup

  1. 01

    Create a connection token

    Name it Script heartbeats. Copy it once.

  2. 02

    Call curl at the start and end of each script

    Post state working when the script starts and idle or routine when it finishes. Post blocked, and needs_you true, when a person has to fix it.

  3. 03

    Post when a long job changes stage

    ttl_seconds is 120. Post when the script starts, finishes, or blocks. A long quiet stretch should show as stale. Do not post the same status every minute.

Snippets

Replace $BOTOPTICON_TOKEN with a connection token. The URL is this app origin plus /api/ingest or /api/mcp.

curl

#!/bin/sh
# Script heartbeats. Posts each tile. Exits non-zero unless curl itself fails.
# Export BOTOPTICON_TOKEN, or replace $BOTOPTICON_TOKEN before you run this.
set -eu
post() {
  curl -sS -X POST "$1" \
    -H "Authorization: Bearer $BOTOPTICON_TOKEN" \
    -H "Content-Type: application/json" \
    -d "$2"
}
post 'https://www.botopticon.com/api/ingest' '{"bot":"Backup","tile":"backup-job","type":"status","title":"Backup job","props":{"state":"working","task":"Copying the database archive"},"needs_you":false,"ttl_seconds":120,"subtitle":"Nightly archive"}'
post 'https://www.botopticon.com/api/ingest' '{"bot":"Ingest","tile":"ingest-job","type":"status","title":"Ingest job","props":{"state":"idle","task":"Waiting for the next event"},"needs_you":false,"ttl_seconds":120,"subtitle":"Idle between posts"}'
post 'https://www.botopticon.com/api/ingest' '{"bot":"Digest","tile":"digest-job","type":"status","title":"Digest job","props":{"state":"routine","task":"Next send at 08:00"},"needs_you":false,"ttl_seconds":120,"subtitle":"On its hour"}'
post 'https://www.botopticon.com/api/ingest' '{"bot":"Sweep","tile":"sweep-job","type":"status","title":"Sweep job","props":{"state":"blocked","task":"Disk is nearly full"},"needs_you":false,"ttl_seconds":120,"subtitle":"Blocked on disk"}'

Agent prompt

Keep the Botopticon board "Script heartbeats" current.
Post each JSON body below to https://www.botopticon.com/api/ingest.
Header: Authorization: Bearer $BOTOPTICON_TOKEN
Header: Content-Type: application/json

If you can call MCP, connect to https://www.botopticon.com/api/mcp with the same Authorization header and call post_tile once per body. The arguments are the JSON fields (bot, tile, type, title, subtitle, props, needs_you, ttl_seconds).

Post when work happens. Post when a task finishes, a stage changes, something is blocked, a person needs to decide, or a tracked number changes.
When needs_you is true, say who is asking, what it is about, and the ask.
Set ttl_seconds to how often you really work. A quiet tile should show as stale.
Use a timer only for data no agent touches, such as an outside metric. Daily is the default.
Never post the same content again just to keep a tile fresh.

{"bot":"Backup","tile":"backup-job","type":"status","title":"Backup job","props":{"state":"working","task":"Copying the database archive"},"needs_you":false,"ttl_seconds":120,"subtitle":"Nightly archive"}
{"bot":"Ingest","tile":"ingest-job","type":"status","title":"Ingest job","props":{"state":"idle","task":"Waiting for the next event"},"needs_you":false,"ttl_seconds":120,"subtitle":"Idle between posts"}
{"bot":"Digest","tile":"digest-job","type":"status","title":"Digest job","props":{"state":"routine","task":"Next send at 08:00"},"needs_you":false,"ttl_seconds":120,"subtitle":"On its hour"}
{"bot":"Sweep","tile":"sweep-job","type":"status","title":"Sweep job","props":{"state":"blocked","task":"Disk is nearly full"},"needs_you":false,"ttl_seconds":120,"subtitle":"Blocked on disk"}

crontab

# Once a day, for a metric no agent touches (Backup job, ttl_seconds 120).
# Skip the post when the value has not changed. Do not use this to repeat a status tile.
0 8 * * * curl -sS -X POST "https://www.botopticon.com/api/ingest" -H "Authorization: Bearer $BOTOPTICON_TOKEN" -H "Content-Type: application/json" -d '{"bot":"Backup","tile":"backup-job","type":"status","title":"Backup job","props":{"state":"working","task":"Copying the database archive"},"needs_you":false,"ttl_seconds":120,"subtitle":"Nightly archive"}'

FAQ

What if a script crashes before the last post?
The pane goes stale when the TTL passes. That is the signal. You do not need a separate down message. A later successful post clears the stale state.
Does this run on the Free plan?
Yes. Every tile in this recipe is a Free type. A Free workspace still has one board and 8 live tiles.
How does the board stay current?
Post when the work changes. Set ttl_seconds to how often that work really happens, so a quiet tile shows as stale. Use a timer only for data no agent touches, and then at most daily. Never post the same content again just to keep a tile fresh.