Skip to content
view raw .md

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_cursor from the body: where the delta feed starts.
  • The ETag header: send it back as If-None-Match to get 304 Not Modified when 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:

  1. states: when it is not null, a column was created, renamed, reordered or archived: replace your whole column list with it.
  2. changed: upsert each task by uuid (one entry per task; the current version wins).
  3. removed: drop each task from the board.
  4. Store cursor and send it verbatim as updated_since next 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_seconds before 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 truncated is true, 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 its delta_cursor.
  • The delta feed allows 240 calls per minute per actor. Following poll_after_seconds keeps 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