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
Filter by project UUID: eq.<uuid>
Sort order: position.asc (default) or position.desc
PostgREST select clause for field projection
Response
Column type: backlog, todo, in_progress, done, or custom
Display position (0-indexed)
Work-in-progress limit for this column
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
Target workflow column UUID
New position rank within the target column
Response
Returns the updated task row with new status_column_id and order_rank.
Updated workflow column UUID
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
Number of days for completion trend (default: 14)
Response
Map of workflow column IDs to task counts {
"uuid-backlog" : 12 ,
"uuid-todo" : 5 ,
"uuid-in-progress" : 3 ,
"uuid-done" : 24
}
Number of tasks past their due date
Number of tasks due in the next 7 days
Array of workload items per assignee Show DashboardWorkloadItem
Number of active tasks assigned
Daily completion counts over the specified window Show CompletionTrendPoint
Number of tasks completed on this date
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 rowCalculate comprehensive project metrics. Parameters:
p_project_id: Project UUID
p_days_window: Days for trend calculation
Returns: Metrics object with counts, workload, and trendsCreate a project with default workflow columns. Parameters:
p_tenant_slug: Tenant identifier
p_name: Project name
p_description: Project description
Returns: Created project UUIDGet the current user’s role in a project. Parameters:
p_project_id: Project UUID
Returns: Role string (owner, editor, viewer)