# Group work into epics

> List a project's epics, create one with a lead team, close it when it's done, and file tickets into it.

Use this when your integration works with bigger pieces of work than a ticket: opening an epic for a feature planned in
another tool, or reporting which teams are behind each epic and how far it's got. By the end you'll know how to list and
create a project's epics, give one a lead team, close it, read its progress and its contributing teams, and file and
list its tickets.

## What an epic is

An **epic** is a deliverable, such as a feature or a migration, that takes several tickets to finish. It belongs to one
project, and each ticket sits in at most one epic, named by the ticket's `epicId`. In the app, epics live under
**Epics & Milestones** (see [Epics and milestones](https://docs.bydawn.com/epics-and-milestones/)).

An epic names the teams behind it in two ways:

- **Its lead team** (`leadTeamId`) is the one team answerable for the epic. You choose it.
- **Its contributing teams** (`contributingTeamIds`) are the teams that own its tickets. By Dawn works them out from the
  tickets, so you can't set them.

The API names a team by its ID, `team-…`, as it does everywhere. To show a team's name, look it up in your
organization's list of teams (see [Put names to team IDs](https://docs.bydawn.com/developers/organize-work-by-team/#put-names-to-team-ids)).

## List a project's epics

`GET /epics` lists a project's epics, oldest first:

```text
GET /epics?projectId=project-3a86…
```

```json
{
  "ok": true,
  "data": [
    {
      "id": "epic-4c2a…",
      "projectId": "project-3a86…",
      "name": "Board export",
      "description": "Let teams take a snapshot of the board out of SAW, as CSV and as a shareable link.",
      "color": "#5B6EE8",
      "targetDate": "2026-11-06",
      "progress": { "totalTickets": 6, "completedTickets": 2 },
      "leadTeamId": "team-1f4e…",
      "contributingTeamIds": ["team-1f4e…", "team-8c3a…"],
      "createdAt": "2026-10-02T09:12:40Z",
      "updatedAt": "2026-10-14T16:05:13Z"
    },
    {
      "id": "epic-9e1b…",
      "projectId": "project-3a86…",
      "name": "Sign in with SSO",
      "color": "#2FA89C",
      "closedAt": "2026-10-09T11:30:00Z",
      "progress": { "totalTickets": 4, "completedTickets": 4 },
      "contributingTeamIds": ["team-8c3a…"],
      "createdAt": "2026-09-15T08:00:21Z",
      "updatedAt": "2026-10-09T11:30:00Z"
    }
  ]
}
```

Done epics are listed too, so check `closedAt`: an open epic has none. A field with no value is left out rather than
sent as `null`, which is why the second epic has no `description`, `targetDate` or `leadTeamId`. An empty list means the
project has no epics yet.

An API key reads one project at a time, so it must pass `projectId`, and without it the list is `403 forbidden`. With a
signed-in member's token, leaving `projectId` out lists the epics of every project you can read.

To read one epic, call `GET /epics/{epicId}`. It answers the same fields.

## Create an epic

Send `projectId` and `name` with `POST /epics`. Everything else is optional:

```json
{ "projectId": "project-3a86…", "name": "Board export", "targetDate": "2026-11-06", "leadTeamId": "team-1f4e…" }
```

- **`description`**: what the epic delivers, in Markdown.
- **`color`**: one of the 12 colors of By Dawn's palette, by its hex, such as `#5B6EE8`. Leave it out and By Dawn picks the
  one that stands out most from the project's open epics.
- **`targetDate`**: when the epic should be done, as a date (`2026-11-06`).
- **`leadTeamId`**: the team leading the epic (see [Choose the lead team](#choose-the-lead-team)).

You get the new epic back. It has no tickets yet, so its progress is zero and it has no contributing teams:

```json
{
  "ok": true,
  "data": {
    "id": "epic-4c2a…",
    "projectId": "project-3a86…",
    "name": "Board export",
    "color": "#5B6EE8",
    "targetDate": "2026-11-06",
    "progress": { "totalTickets": 0, "completedTickets": 0 },
    "leadTeamId": "team-1f4e…",
    "contributingTeamIds": [],
    "createdAt": "2026-10-02T09:12:40Z",
    "updatedAt": "2026-10-02T09:12:40Z"
  }
}
```

The name can't be blank and can be at most 200 characters. A project that doesn't exist in your organization is
refused with `404 invalid_project`, and nothing is created.

## Choose the lead team

The lead team must be a team of your organization that isn't archived. Unlike a ticket's team, it doesn't have to work
in the epic's project, so a team can lead an epic in a project it doesn't otherwise work in. Any other team is refused
with `422`:

```json
{ "ok": false, "error": "invalid_team", "message": "the team is not a live team of this organization" }
```

Branch on the `invalid_team` slug, never on the message (see [Responses and errors](https://docs.bydawn.com/developers/responses-and-errors/)).

To hand the epic to another team, send the new `leadTeamId` with `PATCH /epics/{epicId}`, or `null` to leave it with
no lead team:

```json
{ "leadTeamId": "team-8c3a…" }
```

When an organization admin archives a team, By Dawn clears it from every epic it led, so those epics come back without a
`leadTeamId`.

## Change an epic

`PATCH /epics/{epicId}` changes only the fields you send, and you get the whole epic back:

```json
{ "name": "Board export and sharing", "targetDate": "2026-11-20" }
```

Send `null` to clear `description`, `targetDate` or `leadTeamId`. An epic always has a color, so clearing `color` makes
By Dawn pick one again, the one that stands out most from the project's other open epics. The name can't be cleared.

## Close and reopen an epic

Send `closed: true` when the work is done. By Dawn marks the epic done and stamps `closedAt`:

```json
{ "closed": true }
```

Send `closed: false` to reopen it, and `closedAt` is left out again. Progress reaching 100% doesn't close an epic,
because more work may still be filed under it, so your integration or a person has to. If the epic's tickets were
launched in the app, closing it also ends the launch.

## Read who's working on it and how far it's got

Two fields are worked out from the epic's tickets, and both leave out archived tickets and tickets waiting in triage:

- **`progress`** counts the epic's tickets (`totalTickets`) and how many of them are done (`completedTickets`).
- **`contributingTeamIds`** lists the teams that own those tickets, sorted. A ticket with no team adds none.

You can't set `contributingTeamIds`. To change it, change the `teamId` of the epic's tickets (see
[Move a ticket to another team](https://docs.bydawn.com/developers/organize-work-by-team/#move-a-ticket-to-another-team)). The lead team is set
separately, so it's only among the contributing teams if it owns at least one of the tickets.

## File tickets into an epic

Send `epicId` with `POST /tickets` to file a new ticket into an epic:

```json
{ "projectId": "project-3a86…", "title": "Export the board as CSV", "epicId": "epic-4c2a…" }
```

To move an existing ticket, send `epicId` with `PATCH /tickets/{ticketId}`, or `null` to take it out of its epic. The
epic must be in the ticket's project, or the request is refused with `422 invalid_epic`. Moving a ticket to another
epic also takes it out of its milestone, because a milestone belongs to one epic.

## List an epic's tickets

Filter `GET /tickets` by `epicId`:

```text
GET /tickets?projectId=project-3a86…&epicId=epic-4c2a…
```

`epicId=none` lists the tickets in no epic, which is useful for finding work that hasn't been planned in yet. The rest of
the list's filters, paging and sorting work as usual (see [List and filter](https://docs.bydawn.com/developers/recipes/list-tickets/)).

## Find a team's epics

There's no route that lists a team's epics. List the project's epics instead, and filter them in your code:

- **The epics a team leads** have its ID in `leadTeamId`.
- **The epics a team works on** have its ID in `contributingTeamIds`.

For a team that works in several projects, list each project in turn. With a signed-in member's token you can leave
`projectId` out and filter every project's epics at once.

## What an API key can do

A key with **Board — read only** can list and read epics, and a key with **Board — read & write** can also create and
change them (see [Scopes](https://docs.bydawn.com/developers/authentication/#scopes)). Every call is checked against the epic's project, so a
key can't read or change the epics of a restricted project, or create one there: the request is `403 forbidden`.

## Errors

| Status | Slug | When |
|---|---|---|
| 404 | `epic_not_found` | The epic in the path doesn't exist in your organization. |
| 404 | `invalid_project` | `POST /epics` names a project that doesn't exist in your organization. |
| 422 | `invalid_team` | The lead team isn't a team of your organization, or it's archived. |
| 422 | `validation_error` | The name is blank or over 200 characters, or the color isn't one of the palette's. `message` says which. |
| 422 | `invalid_epic` | A ticket's `epicId` names an epic in another project. |
| 403 | `forbidden` | A key listed epics without `projectId`, used a project it can't see, or wrote without **Board — read & write**. |

None of these is worth retrying unchanged. [Responses and errors](https://docs.bydawn.com/developers/responses-and-errors/) explains the
envelope and the slugs.

## Do and don't

- **Do** store an epic's ID, not its name. People rename epics.
- **Do** close an epic when its work is done. It stays open at 100% until someone does.
- **Don't** try to set an epic's contributing teams. Change its tickets' `teamId` instead.
- **Don't** expect the lead team to be among the contributing teams. It's only there if it owns one of the tickets.

## What isn't in the API yet

- **Deleting an epic.** Close it instead, or delete it in the app (see
  [Delete an epic](https://docs.bydawn.com/epics-and-milestones/#delete-an-epic)).
- **An epic's milestones.** Tickets carry their `milestoneId`, but milestones are created and changed in the app.
- **Launching an epic's tickets, and an epic's activity.** Both are in the app (see
  [Launch an epic](https://docs.bydawn.com/epics-and-milestones/#launch-an-epic)).

## Where to go next

- [Epics and milestones](https://docs.bydawn.com/epics-and-milestones/): what an epic is in the app, with its milestones and launches.
- [Organize work by team](https://docs.bydawn.com/developers/organize-work-by-team/): a project's teams, and a ticket's `teamId`.
- [List and filter](https://docs.bydawn.com/developers/recipes/list-tickets/): the other filters `GET /tickets` takes.
- The [API reference](https://docs.bydawn.com/reference/) lists the epic routes under **Epics**.

Source: https://docs.bydawn.com/developers/epics/
