# Conventions for Plan

> Pagination, the shared filter grammar, sorting, include, errors, rate limits, idempotency, concurrency and conditional reads in the Dailybot Plan API (Beta).

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

---

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

The Plan API follows the [conventions shared by every Dailybot API](/developers/conventions) and adds a few of its own: a filter grammar shared by lists, boards and the timeline; `Idempotency-Key` on creates; `If-Match` for concurrent edits; and `ETag` / `304` on the heaviest reads. This page covers each one once.

<h2 id="pagination">Pagination</h2>

Lists use pages. Send `page` (1-based) and `page_size`, or the aliases `limit` and `offset`:

```bash
curl -sS "https://api.dailybot.com/v1/plan/tasks/?board=ENG&page=2&page_size=100" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN"
```

Every list answers the same envelope:

```json
{
  "count": 152,
  "next": "https://api.dailybot.com/v1/plan/tasks/?board=ENG&page=3&page_size=100",
  "previous": "https://api.dailybot.com/v1/plan/tasks/?board=ENG&page=1&page_size=100",
  "results": []
}
```

- `page_size` defaults to **50** and goes up to **100**. Larger values are clamped, never rejected: asking for 500 returns 100.
- The board snapshot returns at most **50** tasks per column, because it carries labels for every card.
- Follow `next` until it is `null`. `results` is always an array.

The board **delta feed is not a page**: it returns a cursor, not `next`. See [the delta feed reference](/developers/api/plan-boards#plan-board-delta).

<h2 id="filters">Filters</h2>

The task list, the board snapshot and the timeline share one filter grammar, so a saved view works on all three. **Repeating a parameter is OR; different parameters are AND**: `?board=ENG&board=OPS&state=open` means "open tasks on ENG or OPS".

| Parameter | Accepts |
|---|---|
| `board` | Board uuids or keys (`ENG`), repeatable |
| `project` · `goal` · `team` | uuids, repeatable. `goal` matches a task's own goal or the one it inherits from its project |
| `state` | State uuids, or the shortcuts `open` (`backlog`, `todo`, `in_progress`) and `done` (`done`, `canceled`) |
| `category` | `backlog`, `todo`, `in_progress`, `done`, `canceled` |
| `priority` | `1` urgent to `5` none, repeatable |
| `owner` | A user uuid, `me` or `unowned`, repeatable: `owner=me&owner=unowned` is your tasks plus the unowned ones |
| `participant` · `created_by` | User uuids, repeatable |
| `label` | Label uuids, repeatable (up to 50) |
| `due_before` · `due_after` · `start_before` · `start_after` · `completed_before` · `completed_after` | ISO dates, inclusive |
| `has_due_date` · `has_start_date` · `has_dates` · `blocked` | `true` or `false` |
| `estimate_min` · `estimate_max` | Integers |
| `search` | Text matched on title and key (up to 256 characters) |
| `updated_since` | ISO timestamp |
| `is_archived` · `include_archived` | `true` or `false`: only archived rows, or archived and live together |

Two spellings to remember:

- **Overdue** is `due_before=<today>&state=open`. There is no `state=overdue`: it answers `400 invalid_filter_value`.
- **Actionable blocked work** is `blocked=true&state=open`. A finished task can still carry a live blocker, so `blocked=true` alone also returns done work.

An unknown parameter is ignored on the task list, but the board snapshot and `GET /v1/plan/activity/` accept only the parameters they document and refuse anything else with `400 invalid_filter_value`. A value the API cannot parse is `400 invalid_filter_value` everywhere. `owner=me` with an organization API key is `400 actor_required` (see [Authentication for Plan](/developers/plan/authentication#api-key)).

<h2 id="sorting">Sorting and include</h2>

`sort` takes one field, `-` prefixed for descending (`sort=-updated_at`). Every ordering adds a stable tiebreak, so a row never appears on two pages. An unsupported value is `400 invalid_sort`, never a silent fallback.

Some reads embed extra data on request with `include`, a comma-separated list. Each endpoint documents its tokens, for example:

| Endpoint | `include` tokens |
|---|---|
| `GET /v1/plan/tasks/{task_id}/` | `children`, `relations`, `participants`, `attachments`, `comment_count`, `activity`, `comments` |
| `GET /v1/plan/projects/` | `progress` |
| `GET /v1/plan/goals/` | `progress`, `projects` |
| `GET /v1/plan/pulse/` | `projects`, `attention`, `activity`, `goal_progress` |

An unknown token is `400 invalid_filter_value`; an empty `include=` is ignored.

<h2 id="errors">Errors</h2>

Errors carry a human-readable `detail` and, for anything you might branch on, a stable machine-readable `code`:

```json
{
  "detail": "This task changed since you loaded it.",
  "code": "version_conflict",
  "extra": { "current_version": 9 }
}
```

Branch on `code`, never on `detail`. Validation errors on a body list messages per field. A missing object and an object from another organization return the **same `404` body**. Every code, with what to do next, is in [Errors for Plan](/developers/plan/errors).

<h2 id="rate-limits">Rate limits</h2>

Limits apply per actor (a person, or an organization API key), per minute:

| Calls | Limit |
|---|---|
| Reads | 120 per minute |
| Writes | 60 per minute |
| Bulk calls | 30 per minute |
| Board delta feed | 240 per minute |

Past a limit, the API answers `429` with a `Retry-After` header: wait that many seconds before retrying. For a live board, poll the delta feed at the `poll_after_seconds` it suggests rather than on a fixed timer.

<h2 id="idempotency">Idempotency</h2>

Creates and many writes accept an `Idempotency-Key` header (each endpoint's **Headers** table says so): a unique string you generate for one intent.

```bash
curl -sS -X POST "https://api.dailybot.com/v1/plan/tasks/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: import-row-42" \
  -d '{"board": "ENG", "title": "Write the migration guide"}'
```

- Sending the **same key with the same body** again returns the first response, performs nothing new, and adds the header `Idempotency-Replayed: true`. Retry with confidence after a timeout.
- Sending the **same key with a different body** is `409 idempotency_key_payload_mismatch`: a key names one intent.
- A repeat sent **while the first call is still running** gets `409 idempotency_in_progress` for up to 120 seconds.
- Keys are remembered for **24 hours**.
- **Bulk requires it**: `POST /v1/plan/tasks/bulk/` without a key is `400 idempotency_key_required`.

<h2 id="concurrency">Concurrent edits</h2>

Every task has an integer `version`. To avoid overwriting someone else's change, send the version you loaded in `If-Match` (quoted) or as the body field `version`:

```bash
curl -sS -X PATCH "https://api.dailybot.com/v1/plan/tasks/ENG-142/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN" \
  -H "Content-Type: application/json" \
  -H 'If-Match: "7"' \
  -d '{"due_date": "2026-10-22"}'
```

- If the task moved on, the answer is `409 version_conflict` with `extra.current_version`: re-read, reconcile, retry.
- Sending both `If-Match` and `version` with different values is `400 version_precondition_ambiguous`.
- Without either, the last write wins.
- Today task update, move and move-to-board check versions.

Saved views are different: `PUT …/views/` replaces your whole list, so it **requires** `If-Match` with the `ETag` from your last read. A stale value is `412 precondition_failed`; a missing one is `428 precondition_required`.

<h2 id="conditional-reads">Conditional reads</h2>

The board snapshot, task detail and the home pulse return an `ETag`. Send it back in `If-None-Match`; if nothing changed, the API answers `304 Not Modified` with an empty body, so you skip parsing a response you already have:

```bash
curl -sS -i "https://api.dailybot.com/v1/plan/boards/$BOARD/board/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN" \
  -H "If-None-Match: $ETAG"
```

<h2 id="dry-run">Previews with dry_run</h2>

Archive endpoints, milestone completion and bulk calls accept `?dry_run=true`. The API computes the consequence and returns it without writing anything, including `consequence`, a sentence meant to be shown to a person before they confirm:

```bash
curl -sS -X POST "https://api.dailybot.com/v1/plan/boards/$BOARD/archive/?dry_run=true" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN"
```

<h2 id="agent-attribution">Agent attribution</h2>

When an agent works through a person's credential, the person is the **author of every write** and the agent is shown as the one who **executed it on their behalf**. Name the agent on each write:

| Write | How to send the name |
|---|---|
| JSON body | The body field `agent_name` (canonical) |
| Multipart, or no body (`DELETE`, archive, restore) | The header `X-Dailybot-Agent-Name`, with the value percent-encoded as UTF-8 |

```bash
curl -sS -X POST "https://api.dailybot.com/v1/plan/tasks/ENG-142/comments/" \
  -H "Authorization: Bearer $DAILYBOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"body": "Migration guide drafted.", "agent_name": "Release agent"}'
```

- If both are present, the body wins. Control characters are stripped and a blank name means no agent.
- The maximum is 128 characters. A longer or undecodable name is `400 invalid_agent_attribution`; it is never truncated.
- Every mutating `/v1/plan/` endpoint (`POST`, `PUT`, `PATCH`, `DELETE`) accepts it. `GET` requests ignore it.
- An agent-type key, which is not bound to a person, gets `400 invalid_agent_attribution` if it sends a name.
- The stamp never changes a permission answer.
- The name resolves against the same agent registry as `/v1/agent-reports/` (name, aliases, avatar).
- A name may use only letters, numbers, spaces and `. - _ ( ) ' # + / & , :`. Anything else, and the name of a deactivated agent, is `400 invalid_agent_attribution`.
- A comment written through an API key or stamped with an agent has `provenance: agent_authored`.

**In responses**, comments, attachments and activity items carry `executed_by_agent` (`{uuid, name, username, avatar}`, or `null`) next to the author or actor. A task gains `executors`, a list of `{uuid, name, username, avatar, first_at, last_at}`, newest first. It is separate from the singular `executor`, which stays the current ball-holder.

**From the Dailybot CLI (4.0.0 and later):** pass `--agent-name` (or set `DAILYBOT_AGENT_NAME`; the flag wins). With neither, a person is acting directly and nothing is stamped. It is an attribution label, never a credential, so it changes no permission. The CLI sends `agent_name` on JSON writes and the header on uploads and body-less writes, never on reads, and refuses names over 128 characters locally.

```bash
dailybot --agent-name "Claude Code" plan task comment ENG-12 "Reproduced and fixed"
```

`task comments` shows `Jane Doe via "Claude Code"`, and `task get` adds an Agents line listing the executors, most recent first. `task brief [--download DIR] [--force] [--json]` reads a whole card for an agent, attachments included.

<h2 id="additive">Additive changes</h2>

New fields, parameters, event types, relation types and state categories can appear at any time during the Beta. Ignore values you do not recognise, and follow the [API changelog](/developers/api-changelog).

---

## 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) (this page)
- [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)
- [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)

