> ## Documentation Index
> Fetch the complete documentation index at: https://docs.8space.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Tasks

> PostgREST endpoints for task management in 8Space

8Space uses Supabase PostgREST for CRUD operations on tasks, assignees, labels, checklists, attachments, and dependencies.

## Types

```typescript packages/app/src/domain/types.ts theme={null}
export type TaskPriority = 'p0' | 'p1' | 'p2';
export type DependencyType = 'FS';

export interface Task {
  id: string;
  projectId: string;
  title: string;
  statusColumnId: string;
  assignees: UserProfile[];
  dueDate?: string | null;
  startDate?: string | null;
  priority: TaskPriority;
  orderRank: number;
  description?: string | null;
  tags: TaskLabel[];
  checklist: TaskChecklistItem[];
  attachments: TaskAttachment[];
  estimate?: number | null;
  completedAt?: string | null;
  isMilestone: boolean;
  createdAt: string;
  updatedAt: string;
}

export interface TaskLabel {
  id: string;
  projectId: string;
  name: string;
  color: string;
}

export interface TaskChecklistItem {
  id: string;
  taskId: string;
  title: string;
  isDone: boolean;
  position: number;
}

export interface TaskAttachment {
  id: string;
  taskId: string;
  url: string;
  title?: string | null;
  createdAt: string;
}

export interface TaskDependency {
  id: string;
  projectId: string;
  predecessorTaskId: string;
  successorTaskId: string;
  type: DependencyType;
}
```

## List Tasks

Query tasks for a project with full hydration including assignees, labels, checklists, and attachments.

```typescript packages/app/src/domain/repositories/supabase.ts theme={null}
async listTasks(projectId: string): Promise<Task[]> {
  // Fetch task rows
  const { data, error } = await supabase
    .from('tasks')
    .select(
      'id,project_id,title,status_column_id,start_date,due_date,priority,order_rank,description,estimate,is_milestone,completed_at,created_at,updated_at'
    )
    .eq('project_id', projectId)
    .order('order_rank', { ascending: true });

  if (error) throw error;

  const taskRows = (data as TaskRow[] | null) ?? [];
  const taskIds = taskRows.map(task => task.id);

  // Hydrate with related data
  const [assigneeResp, labelResp, checklistResp, attachmentResp] = await Promise.all([
    supabase
      .from('task_assignees')
      .select('task_id,user_id,profile:profiles(id,display_name,avatar_url)')
      .in('task_id', taskIds),
    supabase
      .from('task_label_links')
      .select('task_id,label:task_labels(id,project_id,name,color)')
      .in('task_id', taskIds),
    supabase.from('task_checklist_items').select('id,task_id,title,is_done,position').in('task_id', taskIds),
    supabase.from('task_attachments').select('id,task_id,url,title,created_at').in('task_id', taskIds),
  ]);

  // Map results to Task objects...
}
```

### Query Parameters

<ParamField query="project_id" type="string" required>
  Filter by project UUID: `eq.<uuid>`
</ParamField>

<ParamField query="order" type="string">
  Sort order: `order_rank.asc` or `order_rank.desc`
</ParamField>

<ParamField query="select" type="string">
  PostgREST select clause for field projection
</ParamField>

### Response

Returns array of task objects with all embedded data.

<ResponseField name="id" type="string">
  Task UUID
</ResponseField>

<ResponseField name="project_id" type="string">
  Parent project UUID
</ResponseField>

<ResponseField name="title" type="string">
  Task title
</ResponseField>

<ResponseField name="status_column_id" type="string">
  Workflow column UUID
</ResponseField>

<ResponseField name="start_date" type="string | null">
  Task start date (ISO 8601)
</ResponseField>

<ResponseField name="due_date" type="string | null">
  Task due date (ISO 8601)
</ResponseField>

<ResponseField name="priority" type="TaskPriority">
  Priority level: `p0`, `p1`, or `p2`
</ResponseField>

<ResponseField name="order_rank" type="number">
  Position for sorting tasks
</ResponseField>

<ResponseField name="description" type="string | null">
  Task description
</ResponseField>

<ResponseField name="estimate" type="number | null">
  Time estimate
</ResponseField>

<ResponseField name="is_milestone" type="boolean">
  Whether task is a milestone
</ResponseField>

<ResponseField name="completed_at" type="string | null">
  Completion timestamp
</ResponseField>

<ResponseField name="created_at" type="string">
  Creation timestamp
</ResponseField>

<ResponseField name="updated_at" type="string">
  Last update timestamp
</ResponseField>

## Create Task

Create a new task with assignees.

```typescript packages/app/src/domain/repositories/supabase.ts theme={null}
async createTask(input: CreateTaskInput): Promise<Task> {
  if (!input.startDate || !input.dueDate) {
    throw new Error('Start date and due date are required.');
  }

  if (input.dueDate < input.startDate) {
    throw new Error('Due date must be the same or later than start date.');
  }

  const payload = {
    project_id: input.projectId,
    title: input.title,
    status_column_id: input.statusColumnId,
    due_date: input.dueDate,
    start_date: input.startDate,
    priority: input.priority ?? 'p1',
    order_rank: input.orderRank ?? 1000,
    description: input.description ?? null,
    estimate: input.estimate ?? null,
    is_milestone: input.isMilestone ?? false,
  };

  const { data, error } = await supabase
    .from('tasks')
    .insert(payload)
    .select(
      'id,project_id,title,status_column_id,start_date,due_date,priority,order_rank,description,estimate,is_milestone,completed_at,created_at,updated_at'
    )
    .single();

  if (error) throw error;

  const createdTask = data as TaskRow;

  // Assign users
  if (input.assigneeIds && input.assigneeIds.length > 0) {
    const links = input.assigneeIds.map(userId => ({ task_id: createdTask.id, user_id: userId }));
    const { error: assigneeError } = await supabase.from('task_assignees').insert(links);
    if (assigneeError) throw assigneeError;
  }

  return fetchTaskById(createdTask.project_id, createdTask.id);
}
```

### Request Body

<ParamField body="project_id" type="string" required>
  Project UUID
</ParamField>

<ParamField body="title" type="string" required>
  Task title
</ParamField>

<ParamField body="status_column_id" type="string" required>
  Workflow column UUID
</ParamField>

<ParamField body="start_date" type="string" required>
  Start date (ISO 8601 date format)
</ParamField>

<ParamField body="due_date" type="string" required>
  Due date (ISO 8601 date format)
</ParamField>

<ParamField body="priority" type="TaskPriority">
  Priority (default: `p1`)
</ParamField>

<ParamField body="order_rank" type="number">
  Position for sorting (default: 1000)
</ParamField>

<ParamField body="description" type="string | null">
  Task description
</ParamField>

<ParamField body="estimate" type="number | null">
  Time estimate
</ParamField>

<ParamField body="is_milestone" type="boolean">
  Milestone flag (default: false)
</ParamField>

### Response

Returns the created task with status 201.

## Update Task

Update task fields inline.

```typescript packages/app/src/domain/repositories/supabase.ts theme={null}
async updateTaskInline(input: UpdateTaskInlineInput): Promise<Task> {
  const { data: currentTask, error: currentError } = await supabase
    .from('tasks')
    .select('id,project_id')
    .eq('id', input.taskId)
    .single();

  if (currentError) throw currentError;

  const taskRef = currentTask as { id: string; project_id: string };
  const updatePayload: Record<string, unknown> = {};

  if (input.title !== undefined) updatePayload.title = input.title;
  if (input.statusColumnId !== undefined) updatePayload.status_column_id = input.statusColumnId;
  if (input.dueDate !== undefined) updatePayload.due_date = input.dueDate;
  if (input.startDate !== undefined) updatePayload.start_date = input.startDate;
  if (input.priority !== undefined) updatePayload.priority = input.priority;
  if (input.orderRank !== undefined) updatePayload.order_rank = input.orderRank;
  if (input.description !== undefined) updatePayload.description = input.description;
  if (input.estimate !== undefined) updatePayload.estimate = input.estimate;
  if (input.completedAt !== undefined) updatePayload.completed_at = input.completedAt;
  if (input.isMilestone !== undefined) updatePayload.is_milestone = input.isMilestone;

  if (Object.keys(updatePayload).length > 0) {
    const { error: updateError } = await supabase.from('tasks').update(updatePayload).eq('id', input.taskId);
    if (updateError) throw updateError;
  }

  // Update assignees if provided
  if (input.assigneeIds !== undefined) {
    const { error: clearError } = await supabase.from('task_assignees').delete().eq('task_id', input.taskId);
    if (clearError) throw clearError;

    if (input.assigneeIds.length > 0) {
      const links = input.assigneeIds.map(userId => ({ task_id: input.taskId, user_id: userId }));
      const { error: assignError } = await supabase.from('task_assignees').insert(links);
      if (assignError) throw assignError;
    }
  }

  return fetchTaskById(taskRef.project_id, input.taskId);
}
```

### Query Parameters

<ParamField query="id" type="string" required>
  Task UUID filter: `eq.<uuid>`
</ParamField>

### Request Body

All fields are optional. Only provided fields will be updated.

<ParamField body="title" type="string">
  Updated task title
</ParamField>

<ParamField body="status_column_id" type="string">
  Updated workflow column UUID
</ParamField>

<ParamField body="due_date" type="string | null">
  Updated due date
</ParamField>

<ParamField body="start_date" type="string | null">
  Updated start date
</ParamField>

<ParamField body="priority" type="TaskPriority">
  Updated priority
</ParamField>

<ParamField body="order_rank" type="number">
  Updated position
</ParamField>

<ParamField body="description" type="string | null">
  Updated description
</ParamField>

<ParamField body="estimate" type="number | null">
  Updated estimate
</ParamField>

<ParamField body="completed_at" type="string | null">
  Completion timestamp
</ParamField>

<ParamField body="is_milestone" type="boolean">
  Milestone flag
</ParamField>

### Response

Returns status 204 (no content) on success.

## Delete Task

Delete a task by ID.

```typescript packages/app/src/domain/repositories/supabase.ts theme={null}
async deleteTask(taskId: string): Promise<void> {
  const { error } = await supabase.from('tasks').delete().eq('id', taskId);
  if (error) throw error;
}
```

### Query Parameters

<ParamField query="id" type="string" required>
  Task UUID filter: `eq.<uuid>`
</ParamField>

### Response

Returns status 204 (no content) on success.

## Manage Task Assignees

### Query Assignees

```typescript theme={null}
const { data, error } = await supabase
  .from('task_assignees')
  .select('task_id,user_id,profile:profiles(id,display_name,avatar_url)')
  .in('task_id', taskIds);
```

### Add Assignees

```typescript theme={null}
const links = assigneeIds.map(userId => ({ task_id: taskId, user_id: userId }));
const { error } = await supabase.from('task_assignees').insert(links);
```

### Remove Assignees

```typescript theme={null}
const { error } = await supabase.from('task_assignees').delete().eq('task_id', taskId);
```

## Task Dependencies

### List Dependencies

```typescript packages/app/src/domain/repositories/supabase.ts theme={null}
async listDependencies(projectId: string): Promise<TaskDependency[]> {
  const { data, error } = await supabase
    .from('task_dependencies')
    .select('id,project_id,predecessor_task_id,successor_task_id,type')
    .eq('project_id', projectId);

  if (error) throw error;

  const rows = (data as TaskDependencyRow[] | null) ?? [];

  return rows.map(row => ({
    id: row.id,
    projectId: row.project_id,
    predecessorTaskId: row.predecessor_task_id,
    successorTaskId: row.successor_task_id,
    type: row.type,
  }));
}
```

### Set Dependencies

Replace all dependencies for a successor task.

```typescript packages/app/src/domain/repositories/supabase.ts theme={null}
async setTaskDependencies(
  projectId: string,
  successorTaskId: string,
  predecessorTaskIds: string[],
  type: DependencyType = 'FS'
): Promise<void> {
  // Clear existing dependencies
  const { error: deleteError } = await supabase
    .from('task_dependencies')
    .delete()
    .eq('project_id', projectId)
    .eq('successor_task_id', successorTaskId);

  if (deleteError) throw deleteError;

  if (predecessorTaskIds.length === 0) return;

  // Insert new dependencies
  const rows = predecessorTaskIds.map(predecessorTaskId => ({
    project_id: projectId,
    predecessor_task_id: predecessorTaskId,
    successor_task_id: successorTaskId,
    type,
  }));

  const { error: insertError } = await supabase.from('task_dependencies').insert(rows);
  if (insertError) throw insertError;
}
```

## React Hook Usage

```typescript packages/app/src/hooks/use-project-data.ts theme={null}
import { useQuery, useMutation } from '@tanstack/react-query';
import { taskRepository } from '@/domain/repositories';

// List tasks
export function useTasks(projectId: string | undefined) {
  return useQuery({
    queryKey: ['projects', projectId, 'tasks'],
    queryFn: async () => {
      if (!projectId) return [];
      return taskRepository.listTasks(projectId);
    },
    enabled: Boolean(projectId),
  });
}

// Create task
export function useCreateTask(projectId: string | undefined) {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (input: Omit<CreateTaskInput, 'projectId'>) => {
      if (!projectId) throw new Error('Project is required');
      return taskRepository.createTask({ ...input, projectId });
    },
    onSettled: async () => {
      if (!projectId) return;
      await queryClient.invalidateQueries({
        queryKey: ['projects', projectId, 'tasks']
      });
    },
  });
}

// Update task with optimistic update
export function useUpdateTaskInline(projectId: string | undefined) {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (input: UpdateTaskInlineInput) => taskRepository.updateTaskInline(input),
    onMutate: async (input) => {
      if (!projectId) return { previous: undefined };

      const key = ['projects', projectId, 'tasks'];
      await queryClient.cancelQueries({ queryKey: key });
      const previous = queryClient.getQueryData<Task[]>(key);

      queryClient.setQueryData<Task[]>(key, (current) => {
        if (!current) return current;
        return current.map(task =>
          task.id !== input.taskId ? task : { ...task, ...input }
        );
      });

      return { previous };
    },
    onError: (_error, _variables, context) => {
      if (!projectId || !context?.previous) return;
      queryClient.setQueryData(['projects', projectId, 'tasks'], context.previous);
    },
    onSettled: async () => {
      if (!projectId) return;
      await queryClient.invalidateQueries({
        queryKey: ['projects', projectId, 'tasks']
      });
    },
  });
}

// Delete task
export function useDeleteTask(projectId: string | undefined) {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (taskId: string) => {
      await taskRepository.deleteTask(taskId);
    },
    onSettled: async () => {
      if (!projectId) return;
      await queryClient.invalidateQueries({
        queryKey: ['projects', projectId, 'tasks']
      });
    },
  });
}
```
