Show a board and keep it fresh
Render a Dailybot Plan board in one request, then keep it current with the delta feed: ETag and 304, server-paced polling, truncation and cursor expiry.
Beta
Plan is in beta. Everything under /plan in the web app, the CLI and agent skill commands for projects, goals, boards and tasks, and the /v1/plan/ public API may change before general availability. Want to try it with your team? Write to [email protected].
A board screen needs two things: the whole board once, then only what changed. The Plan API gives you exactly that: a snapshot that renders every column in one request, and a delta feed that returns the tasks changed since a cursor. Poll the feed; never re-read the whole board on a timer.
1. Read the board once
curl -sS -D headers.txt "https://api.dailybot.com/v1/plan/boards/$BOARD/board/" \
-H "Authorization: Bearer $DAILYBOT_TOKEN" > board.json
The response has one entry in groups per column, in column order. Each group carries the column’s true task_count, its first tasks in rank order and has_more when the column holds more cards than the page (up to 50 per column, set with tasks_per_state). Keep two things:
delta_cursorfrom the body: where the delta feed starts.- The
ETagheader: send it back asIf-None-Matchto get304 Not Modifiedwhen nothing changed.
To page the rest of a long column, use the task list with the same board and state: GET /v1/plan/tasks/?board=$BOARD&state=<state uuid>.
2. Ask only for what changed
curl -sS "https://api.dailybot.com/v1/plan/boards/$BOARD/delta/?updated_since=$CURSOR" \
-H "Authorization: Bearer $DAILYBOT_TOKEN"
{
"since": "2026-09-25T10:14:02.113954Z",
"cursor": "2026-09-25T10:19:44.902311Z",
"changed": [ { "uuid": "00000000-0000-4000-8000-000000000005", "key": "ENG-142", "state": { "name": "Done", "category": "done" }, "version": 9 } ],
"removed": [ { "uuid": "00000000-0000-4000-8000-000000000105", "key": "ENG-77", "reason": "archived" } ],
"states": null,
"truncated": false,
"poll_after_seconds": 15
}
Apply it in this order:
states: when it is notnull, a column was created, renamed, reordered or archived: replace your whole column list with it.changed: upsert each task byuuid(one entry per task; the current version wins).removed: drop each task from the board.- Store
cursorand send it verbatim asupdated_sincenext time. Never compute a cursor from your own clock.
Delivery is at-least-once: a task changed at the same instant as your cursor can arrive twice, so upserts must be idempotent.
3. Let the server set the pace
- Wait
poll_after_secondsbefore the next call. It starts at 15 seconds, doubles up to 120 while the board is quiet and drops back as soon as something changes. - If
truncatedistrue, more changes are waiting: poll again immediately. - Pause while the page is hidden and poll once when it becomes visible again.
- A cursor older than 7 days returns
400 delta_window_expired: read the snapshot again and start over from itsdelta_cursor. - The delta feed allows 240 calls per minute per actor. Following
poll_after_secondskeeps you far below it.
A complete polling loop
A shell loop for a script or a terminal dashboard:
CURSOR=$(jq -r .delta_cursor board.json)
while true; do
RESPONSE=$(curl -sS "https://api.dailybot.com/v1/plan/boards/$BOARD/delta/?updated_since=$CURSOR" \
-H "Authorization: Bearer $DAILYBOT_TOKEN")
if [ "$(echo "$RESPONSE" | jq -r '.code // empty')" = "delta_window_expired" ]; then
echo "Cursor expired: read the snapshot again"; break
fi
echo "$RESPONSE" | jq -c '{changed: [.changed[].key], removed: [.removed[].key]}'
CURSOR=$(echo "$RESPONSE" | jq -r .cursor)
if [ "$(echo "$RESPONSE" | jq -r .truncated)" = "true" ]; then continue; fi
sleep "$(echo "$RESPONSE" | jq -r .poll_after_seconds)"
done
The same loop in a Node.js service (Node 18 or later), with the token kept on the server:
const API = 'https://api.dailybot.com/v1/plan';
const headers = { Authorization: `Bearer ${process.env.DAILYBOT_TOKEN}` };
const sleep = (s) => new Promise((resolve) => setTimeout(resolve, s * 1000));
async function follow(boardId, onChange) {
let res = await fetch(`${API}/boards/${boardId}/board/`, { headers });
let cursor = (await res.json()).delta_cursor;
for (;;) {
res = await fetch(`${API}/boards/${boardId}/delta/?updated_since=${encodeURIComponent(cursor)}`, { headers });
const delta = await res.json();
if (delta.code === 'delta_window_expired') return follow(boardId, onChange);
onChange(delta); // replace columns if delta.states, upsert delta.changed, drop delta.removed
cursor = delta.cursor;
if (!delta.truncated) await sleep(delta.poll_after_seconds);
}
}
In a browser, call your own backend rather than putting a Dailybot token in page code, and pause the loop on visibilitychange while the tab is hidden.
Reference
GET /v1/plan/boards/{board_id}/board/: the snapshotGET /v1/plan/boards/{board_id}/delta/: the delta feed- Conventions for Plan: ETag and
304