# 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.

Language: en
Canonical: https://www.dailybot.com/developers/plan/recipes/board-live-updates
Markdown: send header `Accept: text/markdown` on any URL to receive Markdown instead of HTML.
Last Updated: 2026-09-25

---

> **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 **support@dailybot.com**.

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.

<h2 id="snapshot">1. Read the board once</h2>

```bash
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>`.

<h2 id="delta">2. Ask only for what changed</h2>

```bash
curl -sS "https://api.dailybot.com/v1/plan/boards/$BOARD/delta/?updated_since=$CURSOR" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN"
```

```json
{
  "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.

<h2 id="pace">3. Let the server set the pace</h2>

- 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.

<h2 id="loop">A complete polling loop</h2>

A shell loop for a script or a terminal dashboard:

```bash
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:

```js
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.

<h2 id="reference">Reference</h2>

- [`GET /v1/plan/boards/{board_id}/board/`](/developers/api/plan-boards#plan-board-snapshot): the snapshot
- [`GET /v1/plan/boards/{board_id}/delta/`](/developers/api/plan-boards#plan-board-delta): the delta feed
- [Conventions for Plan](/developers/plan/conventions#conditional-reads): ETag and `304`

---

## Developer portal navigation

**Getting Started**

- [Overview](/developers)
- [Quick start](/developers/getting-started)
- [Authentication](/developers/authentication)

**API Reference**

- [API Overview](/developers/api)
- [Users](/developers/api/users)
- [Organization](/developers/api/organization)
- [Teams](/developers/api/teams)
- [Invitations](/developers/api/invitations)
- [Check-ins](/developers/api/check-ins)
- [Forms](/developers/api/forms)
- [Labels](/developers/api/labels)
- [Report channels](/developers/api/report-channels)
- [Templates](/developers/api/templates)
- [Kudos](/developers/api/kudos)
- [Mood tracking](/developers/api/mood)
- [Important dates](/developers/api/important-dates)
- [Messaging](/developers/api/messaging)
- [Automations](/developers/api/workflows)
- [Webhooks](/developers/api/webhooks)
- [Commands platform](/developers/api/commands-platform)
- [Agents](/developers/api/agents)
- [OAuth2](/developers/api/oauth2)
- [Integrations](/developers/api/integrations)
- [CLI](/developers/api/cli)
- [Plan · Projects](/developers/api/plan-projects)
- [Plan · Goals](/developers/api/plan-goals)
- [Plan · Boards](/developers/api/plan-boards)
- [Plan · Tasks](/developers/api/plan-tasks)
- [Plan · Comments & files](/developers/api/plan-collaboration)
- [Plan · Home & search](/developers/api/plan-home)
- [Plan · Notifications & reports](/developers/api/plan-notifications)

**Dailybot Plan**

- [Overview](/developers/plan)
- [Concepts](/developers/plan/concepts)
- [Quickstart](/developers/plan/quickstart)
- [Authentication & scopes](/developers/plan/authentication)
- [Agents on Plan](/developers/plan/agents)
- [Conventions](/developers/plan/conventions)
- [Errors](/developers/plan/errors)
- [CLI for Plan](/developers/plan/cli)
- [Agent skill](/developers/plan/agent-skill)
- [Recipe: live board](/developers/plan/recipes/board-live-updates) (this page)
- [Recipe: home in one request](/developers/plan/recipes/home-in-one-request)
- [Recipe: bulk create](/developers/plan/recipes/bulk-create)
- [Recipe: move on PR merge](/developers/plan/recipes/move-on-pr-merge)
- [Recipe: goal progress](/developers/plan/recipes/goal-progress)
- [Recipe: webhooks](/developers/plan/recipes/webhooks)

**API guides**

- [Errors & Status Codes](/developers/errors)
- [Rate Limits](/developers/rate-limits)
- [Conventions](/developers/conventions)
- [API Changelog](/developers/api-changelog)
- [Recipes](/developers/recipes)

**Developer Features**

- [Custom commands](/developers/custom-commands)
- [Serverless commands](/developers/serverless)
- [Webhooks & events](/developers/webhooks)
- [Automation API trigger](/developers/workflow-trigger)
- [Activity API](/developers/activity-api)

**CLI**

- [Overview](/developers/cli)
- [Authentication](/developers/cli-authentication)
- [Command reference](/developers/cli-reference)
- [CI/CD recipes](/developers/cli-ci-cd)
- [Configuration](/developers/cli-configuration)
- [Troubleshooting](/developers/cli-troubleshooting)

**Agent Skill**

- [Overview](/developers/agent-skill)
- [Skills catalog](/skills)

---

## Site navigation

**Product:**
- [Home](/)
- [Product](/product)
- [Pricing](/pricing)
- [Enterprise](/enterprise)
- [Integrations](/integrations)
- [Templates](/templates)

**Resources:**
- [Blog](/blog)
- [Academy](/academy)
- [Changelog](/changelog)
- [Help Center](/help)
- [Developers](/developers)
- [Agents](/agents)

**Company:**
- [About](/about)
- [Careers](/careers)
- [Security](/security)
- [Contact Sales](/demo)

**Connect:**
- [LinkedIn](https://www.linkedin.com/company/dailybot/)
- [X/Twitter](https://twitter.com/dailybot)
- [GitHub](https://github.com/Dailybot-Inc)
- [YouTube](https://www.youtube.com/channel/UC3uM9V52vwX7e3vQpCc4qvA)

