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

# Workflow

> PostgREST endpoints and RPC functions for workflow management in 8Space

8Space uses workflow columns to organize tasks in a Kanban-style board. This page documents endpoints for managing columns, moving tasks, and calculating project metrics.

## Types

```typescript packages/app/src/domain/types.ts theme={null}
export type WorkflowColumnKind = 'backlog' | 'todo' | 'in_progress' | 'done' | 'custom';

export interface WorkflowColumn {
  id: string;
  projectId: string;
  name: string;
  kind: WorkflowColumnKind;
  position: number;
  wipLimit?: number | null;
  definitionOfDone?: string | null;
}

export interface ProjectMetrics {
  tasksByStatus: Record<string, number>;
  overdueCount: number;
  dueThisWeek: number;
  workloadByAssignee: DashboardWorkloadItem[];
  completionTrend: CompletionTrendPoint[];
}

export interface DashboardWorkloadItem {
  userId: string;
  displayName: string;
  activeCount: number;
}

export interface CompletionTrendPoint {
  date: string;
  doneCount: number;
}
```

## List Workflow Columns

Query workflow columns for a project, ordered by position.

```typescript packages/app/src/domain/repositories/supabase.ts theme={null}
async listWorkflowColumns(projectId: string): Promise<WorkflowColumn[]> {
  const { data, error } = await supabase
    .from('workflow_columns')
    .select('id,project_id,name,kind,position,wip_limit,definition_of_done')
    .eq('project_id', projectId)
    .order('position', { ascending: true });

  if (error) throw error;

  return ((data as WorkflowColumnRow[] | null) ?? []).map(row => ({
    id: row.id,
    projectId: row.project_id,
    name: row.name,
    kind: row.kind,
    position: row.position,
    wipLimit: row.wip_limit,
    definitionOfDone: row.definition_of_done,
  }));
}
```

### Query Parameters

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

<ParamField query="order" type="string">
  Sort order: `position.asc` (default) or `position.desc`
</ParamField>

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

### Response

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

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

<ResponseField name="name" type="string">
  Column display name
</ResponseField>

<ResponseField name="kind" type="WorkflowColumnKind">
  Column type: `backlog`, `todo`, `in_progress`, `done`, or `custom`
</ResponseField>

<ResponseField name="position" type="integer">
  Display position (0-indexed)
</ResponseField>

<ResponseField name="wip_limit" type="integer | null">
  Work-in-progress limit for this column
</ResponseField>

<ResponseField name="definition_of_done" type="string | null">
  Criteria for task completion in this column
</ResponseField>

## Move Task (RPC)

Move a task to a different column and set its rank using the `move_task` RPC function.

```typescript packages/app/src/domain/repositories/supabase.ts theme={null}
async moveTask(taskId: string, toColumnId: string, newRank: number): Promise<Task> {
  const { data, error } = await supabase.rpc('move_task', {
    p_task_id: taskId,
    p_to_column_id: toColumnId,
    p_new_rank: newRank,
  });

  if (error) throw error;

  const row = requireData(data as TaskRow | null, 'move_task returned no data');
  return fetchTaskById(row.project_id, row.id);
}
```

### Request Body

<ParamField body="p_task_id" type="string" required>
  Task UUID to move
</ParamField>

<ParamField body="p_to_column_id" type="string" required>
  Target workflow column UUID
</ParamField>

<ParamField body="p_new_rank" type="number" required>
  New position rank within the target column
</ParamField>

### Response

Returns the updated task row with new `status_column_id` and `order_rank`.

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

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

<ResponseField name="order_rank" type="number">
  Updated position rank
</ResponseField>

## Reorder Tasks

Reorder multiple tasks within or across columns by updating their rank.

```typescript packages/app/src/domain/repositories/supabase.ts theme={null}
async reorderTasks(_projectId: string, orderedTaskIds: string[]): Promise<void> {
  if (orderedTaskIds.length === 0) return;

  const updates = orderedTaskIds.map((taskId, index) =>
    supabase
      .from('tasks')
      .update({ order_rank: (index + 1) * 1000 })
      .eq('id', taskId)
  );

  const results = await Promise.all(updates);
  const failed = results.find(result => result.error);
  if (failed?.error) throw failed.error;
}
```

### Implementation

This method updates the `order_rank` field for multiple tasks. Each task receives a rank based on its position in the array: `(index + 1) * 1000`.

## Dashboard Metrics (RPC)

Calculate project metrics including task counts, overdue tasks, and completion trends.

```typescript packages/app/src/domain/repositories/supabase.ts theme={null}
async getProjectMetrics(projectId: string, rangeDays: number): Promise<ProjectMetrics> {
  const { data, error } = await supabase.rpc('dashboard_metrics', {
    p_project_id: projectId,
    p_days_window: rangeDays,
  });

  if (error) throw error;

  const metrics = (data ?? {}) as DashboardMetricsRpc;

  return {
    tasksByStatus: metrics.tasksByStatus ?? {},
    overdueCount: metrics.overdueCount ?? 0,
    dueThisWeek: metrics.dueThisWeek ?? 0,
    workloadByAssignee: metrics.workloadByAssignee ?? [],
    completionTrend: metrics.completionTrend ?? [],
  };
}
```

### Request Body

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

<ParamField body="p_days_window" type="integer">
  Number of days for completion trend (default: 14)
</ParamField>

### Response

<ResponseField name="tasksByStatus" type="object">
  Map of workflow column IDs to task counts

  <Expandable title="example">
    ```json theme={null}
    {
      "uuid-backlog": 12,
      "uuid-todo": 5,
      "uuid-in-progress": 3,
      "uuid-done": 24
    }
    ```
  </Expandable>
</ResponseField>

<ResponseField name="overdueCount" type="integer">
  Number of tasks past their due date
</ResponseField>

<ResponseField name="dueThisWeek" type="integer">
  Number of tasks due in the next 7 days
</ResponseField>

<ResponseField name="workloadByAssignee" type="array">
  Array of workload items per assignee

  <Expandable title="DashboardWorkloadItem">
    <ResponseField name="userId" type="string">
      Assignee UUID
    </ResponseField>

    <ResponseField name="displayName" type="string">
      Assignee display name
    </ResponseField>

    <ResponseField name="activeCount" type="integer">
      Number of active tasks assigned
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="completionTrend" type="array">
  Daily completion counts over the specified window

  <Expandable title="CompletionTrendPoint">
    <ResponseField name="date" type="string">
      Date in ISO 8601 format
    </ResponseField>

    <ResponseField name="doneCount" type="integer">
      Number of tasks completed on this date
    </ResponseField>
  </Expandable>
</ResponseField>

## React Hook Usage

### Workflow Columns

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

export function useWorkflowColumns(projectId: string | undefined) {
  return useQuery({
    queryKey: ['projects', projectId, 'columns'],
    queryFn: async () => {
      if (!projectId) return [];
      return projectRepository.listWorkflowColumns(projectId);
    },
    enabled: Boolean(projectId),
  });
}
```

### Move Task

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

export function useMoveTask(projectId: string | undefined) {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (input: { taskId: string; toColumnId: string; newRank: number }) =>
      taskRepository.moveTask(input.taskId, input.toColumnId, input.newRank),
    onMutate: async (input) => {
      if (!projectId) return { previous: undefined };

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

      // Optimistic update
      queryClient.setQueryData<Task[]>(key, (current) => {
        if (!current) return current;
        return current.map(task =>
          task.id === input.taskId
            ? { ...task, statusColumnId: input.toColumnId, orderRank: input.newRank }
            : task
        );
      });

      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'] });
      await queryClient.invalidateQueries({ queryKey: ['projects', projectId, 'metrics'] });
    },
  });
}
```

### Dashboard Metrics

```typescript packages/app/src/hooks/use-dashboard-metrics.ts theme={null}
import { useQuery } from '@tanstack/react-query';
import { dashboardRepository } from '@/domain/repositories';

export function useDashboardMetrics(projectId: string | undefined, daysWindow = 14) {
  return useQuery({
    queryKey: ['projects', projectId, 'metrics', daysWindow],
    queryFn: async () => {
      if (!projectId) {
        return {
          tasksByStatus: {},
          overdueCount: 0,
          dueThisWeek: 0,
          workloadByAssignee: [],
          completionTrend: [],
        };
      }

      return dashboardRepository.getProjectMetrics(projectId, daysWindow);
    },
    enabled: Boolean(projectId),
  });
}
```

## Example: Kanban Board Integration

```typescript theme={null}
import { useMoveTask, useWorkflowColumns, useTasks } from '@/hooks/use-project-data';

function KanbanBoard({ projectId }: { projectId: string }) {
  const { data: columns = [] } = useWorkflowColumns(projectId);
  const { data: tasks = [] } = useTasks(projectId);
  const moveTask = useMoveTask(projectId);

  const handleDrop = (taskId: string, toColumnId: string, index: number) => {
    const newRank = (index + 1) * 1000;
    moveTask.mutate({ taskId, toColumnId, newRank });
  };

  return (
    <div className="kanban-board">
      {columns.map(column => (
        <div key={column.id} className="kanban-column">
          <h3>{column.name}</h3>
          {column.wipLimit && <span>WIP: {column.wipLimit}</span>}
          {tasks
            .filter(task => task.statusColumnId === column.id)
            .map((task, index) => (
              <TaskCard
                key={task.id}
                task={task}
                onDrop={(toColumnId) => handleDrop(task.id, toColumnId, index)}
              />
            ))}
        </div>
      ))}
    </div>
  );
}
```

## RPC Functions Summary

8Space uses these PostgreSQL RPC functions for complex operations:

<Tabs>
  <Tab title="move_task">
    Move a task to a different column and set its rank atomically.

    **Parameters:**

    * `p_task_id`: Task UUID
    * `p_to_column_id`: Target column UUID
    * `p_new_rank`: New position rank

    **Returns:** Updated task row
  </Tab>

  <Tab title="dashboard_metrics">
    Calculate comprehensive project metrics.

    **Parameters:**

    * `p_project_id`: Project UUID
    * `p_days_window`: Days for trend calculation

    **Returns:** Metrics object with counts, workload, and trends
  </Tab>

  <Tab title="create_project_with_defaults">
    Create a project with default workflow columns.

    **Parameters:**

    * `p_tenant_slug`: Tenant identifier
    * `p_name`: Project name
    * `p_description`: Project description

    **Returns:** Created project UUID
  </Tab>

  <Tab title="current_project_role">
    Get the current user's role in a project.

    **Parameters:**

    * `p_project_id`: Project UUID

    **Returns:** Role string (`owner`, `editor`, `viewer`)
  </Tab>
</Tabs>
