Skip to main content
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

packages/app/src/domain/types.ts
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.
packages/app/src/domain/repositories/supabase.ts
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

project_id
string
required
Filter by project UUID: eq.<uuid>
order
string
Sort order: position.asc (default) or position.desc
select
string
PostgREST select clause for field projection

Response

id
string
Workflow column UUID
project_id
string
Parent project UUID
name
string
Column display name
kind
WorkflowColumnKind
Column type: backlog, todo, in_progress, done, or custom
position
integer
Display position (0-indexed)
wip_limit
integer | null
Work-in-progress limit for this column
definition_of_done
string | null
Criteria for task completion in this column

Move Task (RPC)

Move a task to a different column and set its rank using the move_task RPC function.
packages/app/src/domain/repositories/supabase.ts
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

p_task_id
string
required
Task UUID to move
p_to_column_id
string
required
Target workflow column UUID
p_new_rank
number
required
New position rank within the target column

Response

Returns the updated task row with new status_column_id and order_rank.
id
string
Task UUID
status_column_id
string
Updated workflow column UUID
order_rank
number
Updated position rank

Reorder Tasks

Reorder multiple tasks within or across columns by updating their rank.
packages/app/src/domain/repositories/supabase.ts
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.
packages/app/src/domain/repositories/supabase.ts
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

p_project_id
string
required
Project UUID
p_days_window
integer
Number of days for completion trend (default: 14)

Response

tasksByStatus
object
Map of workflow column IDs to task counts
overdueCount
integer
Number of tasks past their due date
dueThisWeek
integer
Number of tasks due in the next 7 days
workloadByAssignee
array
Array of workload items per assignee
completionTrend
array
Daily completion counts over the specified window

React Hook Usage

Workflow Columns

packages/app/src/hooks/use-project-data.ts
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

packages/app/src/hooks/use-project-data.ts
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

packages/app/src/hooks/use-dashboard-metrics.ts
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

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:
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

Build docs developers (and LLMs) love