# Work items

> Issues and tasks on pipelines, with identifiers, parents without cycles, several assignees, priority 0 to 4, estimates, start, end and due dates, locking, projects, iterations, batched edits and a progress tree.

Source: https://bettersupabase.com/docs/blocks/work-items

`work-items` is the task and issue record of Linear, Jira, GitHub issues and monday items: a title, a body, a parent, assignees, a priority, an estimate, start, end and due dates, and a stage on a [pipeline](/docs/blocks/pipelines). Items can belong to a team, a project and an iteration (a cycle or a sprint). Their identifier is `TASK-12`, or the format of a `work_item` entity in [number-sequences](/docs/blocks/number-sequences).

Reading needs `work_items.read`, creating, editing and assigning needs `work_items.write`, and projects, iterations and deleting someone else's item need `work_items.manage`. Admins get every permission, and members get `work_items.read` and `work_items.write`.

```bash
better-supabase sql add work-items
```

The module needs `tenant`, `access`, `subjects` and `pipelines`. Installing it binds the `work_item` type in the pipelines module with one pipeline per team (`scope: "team_id"`) and the categories of Linear's workflow states:

| Category    | Outcome | Default stages         |
| ----------- | ------- | ---------------------- |
| `triage`    | open    | Triage                 |
| `backlog`   | open    | Backlog                |
| `unstarted` | open    | Todo                   |
| `started`   | open    | In progress, In review |
| `completed` | success | Done                   |
| `canceled`  | failure | Canceled               |

A stage's category sets the item's `state`, so a list or a policy reads `state` and never a stage name. Set `sql.modules.pipelines.options.subjects.work_item` to rename the stages or start with your own, such as CentraKit's Todo, In progress, On hold and Done; your keys are merged over the defaults.

```ts title="better-supabase.config.ts"
import { workItemsSubjects } from "better-supabase/blocks/work-items";

export default defineConfig({
  subjects: { ...workItemsSubjects({ url: "/tasks/{id}" }) },
});
```

## Create, edit and assign [#create-edit-and-assign]

```ts
import { createWorkItems } from "better-supabase/blocks/work-items";

const items = createWorkItems({ transport });
const epic = await items
  .create(organizationId, { title: "Checkout v2", teamId, priority: 2 })
  .orThrow();
const task = await items
  .create(
    organizationId,
    { title: "Payment API", parentId: epic.id, teamId, dueAt },
    [userId],
  )
  .orThrow();

const { accepted, rejected } = await items
  .update([
    { workItemId: task.id, field: "stageId", value: inProgressStageId },
    { workItemId: task.id, field: "endAt", value: friday },
  ])
  .orThrow();
```

`update(changes)` applies grid edits and Gantt drags one by one and returns the refused ones with their hint. The table refuses a parent that would put an item under itself (`WORK_ITEM_PARENT_CYCLE`) and date changes on a locked item (`WORK_ITEM_LOCKED`). Moving to a completed or canceled stage stamps `closedAt` and a close reason (`completed`, or `not_planned` for canceled; pass `closeReason: "duplicate"` with the move to mark a duplicate) and records `work_item.closed`. Moving back to an open stage clears both and records `work_item.reopened`.

`assign(id, userIds)` replaces the assignees. Each one watches the item, and the people newly assigned get a `work_item.assigned` notification. `options.maxAssignees` caps the list (`WORK_ITEM_ASSIGNEES_LIMIT`), and `1` gives the single-assignee shape of Linear and Jira.

## Lists, trees, projects and iterations [#lists-trees-projects-and-iterations]

`list(organizationId, { teamId, projectId, iterationId, parentId, assigneeId, states, open, search })` returns active items, newest first, 50 per page; pass the last item's id as `after` for the next page. `tree(rootId)` returns an item and everything under it with its depth; an item without its own `progress` rolls up the share of its open and completed children that are completed, for a Gantt group.

Dependencies between items are [relations](/docs/blocks/relations): `link({ kind: "blocks", source: design, target: build })` is a finish-to-start dependency, and `edges(organizationId, "blocks")` reads them for the chart.

`saveProject(organizationId, { key, name, status, startOn, targetOn })` keeps a project (`PAY`) with the share of its items that are completed. `saveIteration(organizationId, { name, teamId, startsOn, endsOn })` keeps a cycle or a sprint. Turn either table off with `tables: { projects: null }` or `tables: { iterations: null }`.

## Extra columns [#extra-columns]

`options.extraColumns` adds columns to `work_items`, such as CentraKit's `customer_id`. Every item a read returns carries them in `fields`.

## Events [#events]

| Event                                                                                                                           | Data                                                                                         |
| ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `work_item.created`, `work_item.updated`, `work_item.assigned`, `work_item.reopened`, `work_item.archived`, `work_item.deleted` | `organizationId`, `workItemId`, `subjectType`, `subjectId`, `title`, `fields`, `assigneeIds` |
| `work_item.closed`                                                                                                              | the same, with `state` and `closeReason`                                                     |

## Hooks [#hooks]

The module calls `before_work_item_create(tenant, attrs jsonb)`, `after_work_item_create(work_item uuid)`, `before_work_item_update(work_item uuid, field text, value jsonb)` and `after_work_item_update(work_item uuid, field text)`. A `before_` hook raises to refuse; in `update`, that refuses only its change.

## Wire it to ReUI [#wire-it-to-reui]

The ReUI Kanban blocks commit a drag through `onValueCommit`: call `rank.move("work_item", id, { previous, next, group: stageId })`. The Gantt blocks commit a move or resize through `onEventUpdate`: send `startAt` and `endAt` through `update`, and return `false` when the change comes back in `rejected` (a locked bar). Read the Gantt rows from `tree`, and its arrows from the `blocks` relations.

## Errors [#errors]

| Hint                                           | Cause                                               |
| ---------------------------------------------- | --------------------------------------------------- |
| `WORK_ITEM_FORBIDDEN`                          | the caller lacks the permission                     |
| `WORK_ITEM_TITLE_REQUIRED`                     | the title is empty                                  |
| `WORK_ITEM_ATTRS_INVALID`                      | a field has an invalid value                        |
| `WORK_ITEM_NOT_FOUND`                          | no item the caller can edit                         |
| `WORK_ITEM_FIELD_UNKNOWN`                      | `update` names a field it doesn't write             |
| `WORK_ITEM_PARENT_CYCLE`                       | the parent would put the item under itself          |
| `WORK_ITEM_PARENT_INVALID`                     | the parent is in another organization               |
| `WORK_ITEM_LOCKED`                             | a locked item's dates don't change                  |
| `WORK_ITEM_ASSIGNEES_LIMIT`                    | more assignees than `maxAssignees`                  |
| `WORK_ITEM_PROJECT_*`, `WORK_ITEM_ITERATION_*` | a project or iteration is missing, taken or invalid |