Back to Insights
10 Jul 20264 min readDev Tools

We Made a GitHub Project Board a Two-Way Live Mirror of Our Agents' State

Run more than one autonomous agent against real work and you end up with the same coordination problem twice: your agents track their own status somewhere (a database row, a queue entry, whatever their own state store looks like), and the humans watching them want that status on a board they already live in — GitHub Projects, usually, because the work is already tracked as issues and PRs there. The naive fix is to have each agent call the GitHub API directly whenever its status changes. That's the version that breaks first.

Why agents calling the board API directly doesn't hold up

Every agent making its own board API calls means every agent needs its own rate-limit awareness, its own retry/backoff logic, and its own idempotency guarantees — and GitHub's secondary rate limits don't care that the caller is three different agents instead of one; they see one abusive-looking client. Worse, a restart mid-write can double-move a card or double-post a comment, because "did I already do this?" isn't answered by anything durable if the state only lives in the agent's own head for that one call.

We made agent-workboard to take that entire problem out of the agent's code path. Your agents keep doing exactly what they already do — write a status to a SQLite table (or emit it from a command, if that's more natural for your stack). Nothing about their code changes. A small, separate daemon reads that state, diffs it against the board, and makes the API calls.

How it actually works

agent-workboard reads work items from a source you configure — a SQLite query is the default, but it also accepts an arbitrary command that emits TSV or JSON, so any existing status store can feed it without being rewritten to match the tool. The contract is five columns, and only two are required: id and status. title, note, and owner_tag are optional but drive the rest of the behavior.

Outbound, the daemon diffs your state against the board on an interval (45 seconds by default) and moves cards to match — creating a draft card the first time it sees a new work item, moving it between columns as status changes, and posting a short "why" comment on the transitions you've flagged as worth explaining (a blocked or done status, typically). Your agent code makes zero GitHub API calls; it just keeps writing to its own table like it always did.

Inbound is the more interesting half. When you comment on a card — "actually, do X first" — agent-workboard picks that up, looks up which agent owns the work item (owner_tag), and routes the directive to that agent through a hook you control: by default it's appended as a line to an outbox file your fleet already consumes, but you can point it at any command (a message bus, a send CLI, whatever delivery mechanism your setup already has) via route_cmd. The card gets a ✓ acknowledgment comment so you can see the directive was actually picked up, not just posted into the void.

   your agents            agent-workboard              GitHub Project board
 ┌──────────────┐   read   ┌───────────────┐  create/move   ┌───────────────┐
 │ write status │────────► │  OUTBOUND     │───────────────►│  ▓ Backlog     │
 │  to sqlite   │  (table  │  diff & move  │   why-comment   │  ▓ In Progress │
 └──────────────┘  or cmd) └───────────────┘                 │  ▓ Done        │
        ▲                  ┌───────────────┐  read comments  └───────────────┘
        │  route directive │  INBOUND      │◄───────────────  owner comments
        └──────────────────│  route + ack  │   ✓ ack
          (hook / outbox)  └───────────────┘

The part that actually took the effort: idempotency across restarts

A daemon that syncs state to a board is easy to write badly — the hard requirement is that a restart, a crash, or two passes running concurrently never re-moves a card that already moved, and never re-routes a directive that was already delivered. Every routed comment is recorded in SQLite keyed by its content (for draft cards, which have no native comment thread — a "comment" there is a body line) or its comment node-id (for real Issues), and the check-and-mark is a single atomic statement. A directive gets routed exactly once, full stop, no matter how many times the daemon restarts in between. All writes to the board itself are paced through an embedded leaky-bucket queue, so a burst of status changes doesn't trip GitHub's secondary rate limit — and if it does anyway, the whole queue pauses and retries without burning through attempts.

There's no model in this loop anywhere. It's a deterministic diff-and-sync daemon, plus a routing rule — which is exactly the right amount of machinery for "keep a board honest," and none of the fragility that comes from asking an LLM to decide when to move a card.

If you're running any kind of autonomous or semi-autonomous agent work and want it visible on a board your team already uses — without wiring GitHub API calls into every agent — get in touch and we'll help you set it up against your own state store.

AI agentsGitHubautomationidempotencyops

Found this useful?