Sprinter Platform

Task System

TaskRecord, human-in-the-loop workflows, agent task assignment, and kanban views.

Overview

The task system provides structured work tracking for both humans and agents. Tasks can be assigned to users or agents, linked to entities, organized in hierarchies, and tracked through standard status transitions.

Tasks power human-in-the-loop workflows: when an automated workflow reaches a step that requires human judgment, it creates a task and waits for completion.

TaskRecord

interface TaskRecord {
  id: string;
  tenant_id: string;
  title: string;
  description: string | null;
  status: TaskStatus;
  assignee_type: "user" | "agent" | null;
  assignee_id: string | null;
  entity_id: string | null;
  parent_task_id: string | null;
  metadata: Record<string, unknown> | null;
  due_at: string | null;
  completed_at: string | null;
  created_by: string | null;
  created_at: string;
  updated_at: string;
}

Task Statuses

import { TASK_STATUSES } from "@sprinterai/core";

// "pending" | "in_progress" | "completed" | "failed" | "cancelled"

Standard lifecycle: pending -> in_progress -> completed (or failed / cancelled).

Task Store

import type { TaskStore } from "@sprinterai/core";

interface TaskStore {
  getTask(id: string): Promise<TaskRecord | null>;
  listTasks(options?: {
    status?: TaskStatus;
    assigneeId?: string;
    entityId?: string;
    limit?: number;
  }): Promise<TaskRecord[]>;
  createTask(input: Omit<TaskRecord, "id" | "created_at" | "updated_at">): Promise<TaskRecord>;
  updateTask(id: string, input: Partial<Pick<TaskRecord,
    "title" | "description" | "status" | "assignee_type" |
    "assignee_id" | "metadata" | "due_at" | "completed_at"
  >>): Promise<TaskRecord>;
}

Creating Tasks

const task = await stores.task.createTask({
  tenant_id: tenantId,
  title: "Review ACME Corp financial analysis",
  description: "Analyst has completed initial research. Review findings and approve.",
  status: "pending",
  assignee_type: "user",
  assignee_id: reviewerId,
  entity_id: acmeCorpEntityId,
  parent_task_id: null,
  metadata: { priority: "high", workflowNodeId: "review-step" },
  due_at: "2026-04-01T00:00:00Z",
  completed_at: null,
  created_by: agentUserId,
});

Human-in-the-Loop

When a workflow reaches a wait_human node, the system:

  1. Creates a task assigned to the appropriate user
  2. Pauses the workflow run
  3. Waits for the task to be completed
  4. Resumes the workflow when the user marks the task as done
import { defineWorkflow } from "@sprinterai/core";

const enrichment = defineWorkflow([
  { id: "research", agentSlug: "researcher", fieldKey: "overview" },
  { id: "review", type: "wait_human", dependsOn: ["research"] },
  { id: "finalize", agentSlug: "analyst", fieldKey: "score", dependsOn: ["review"] },
]);

The review node creates a task and waits. When the user completes the task (via UI or API), the workflow resumes with the finalize node.

Agent Task Assignment

Tasks can be assigned to agents for autonomous execution:

const task = await stores.task.createTask({
  tenant_id: tenantId,
  title: "Extract revenue data for Q4",
  status: "pending",
  assignee_type: "agent",
  assignee_id: analystAgentId,
  entity_id: companyEntityId,
  metadata: { fieldKey: "q4_revenue" },
  // ...other fields
});

Heartbeat agents can pick up assigned tasks during their scheduled runs.

Task Hierarchies

Tasks support parent-child relationships for decomposing complex work:

// Parent task
const parentTask = await stores.task.createTask({
  title: "Complete due diligence for ACME Corp",
  status: "in_progress",
  // ...
});

// Child tasks
await stores.task.createTask({
  title: "Financial analysis",
  parent_task_id: parentTask.id,
  assignee_type: "agent",
  assignee_id: analystId,
  // ...
});

await stores.task.createTask({
  title: "Legal review",
  parent_task_id: parentTask.id,
  assignee_type: "user",
  assignee_id: lawyerId,
  // ...
});

Querying Tasks

// Get all pending tasks for a user
const myTasks = await stores.task.listTasks({
  assigneeId: userId,
  status: "pending",
});

// Get all tasks for an entity
const entityTasks = await stores.task.listTasks({
  entityId: companyEntityId,
});

// Get agent's backlog
const agentTasks = await stores.task.listTasks({
  assigneeId: agentId,
  status: "pending",
  limit: 10,
});

Task Metadata

The metadata JSONB field stores arbitrary structured data:

// Workflow context
metadata: {
  workflowRunId: "uuid",
  workflowNodeId: "review-step",
  priority: "high",
}

// Agent extraction context
metadata: {
  fieldKey: "revenue",
  entityTypeSlug: "company",
  extractionSource: "web",
}

// Review context
metadata: {
  reviewType: "approval",
  requiredApprovers: 2,
  currentApprovals: 1,
}

In-Memory Store (Testing)

import { InMemoryTaskStore } from "@sprinterai/runtime";

const taskStore = new InMemoryTaskStore();
const task = await taskStore.createTask({ /* ... */ });

Supabase Store (Production)

import { SupabaseTaskStore } from "@sprinterai/supabase";

const taskStore = new SupabaseTaskStore(supabaseClient, tenantId);

Registry Blocks

Three registry blocks ship the full task UI. Install via npx shadcn add @sprinterai/<name>.

task-hub

Full task management UI with three views switchable from a persistent header:

  • Inbox — collapsible sections for Needs Attention (waiting_human), In Progress, and Completed. Per-task approve / deny / retry actions.
  • Board — drag-and-drop kanban with group-by status, priority, or assignee type.
  • Planner — due-date timeline: unscheduled sidebar + day-grouped scheduled list.
import { TaskHub } from "@/components/task-hub/task-hub";

<TaskHub
  tasks={tasks}
  view={view}
  onViewChange={setView}
  onApprove={(id) => updateTask(id, { status: "in_progress" })}
  onDeny={(id) => updateTask(id, { status: "cancelled" })}
  onStatusChange={(id, status) => updateTask(id, { status })}
  onTaskClick={(task) => router.push(`/tasks/${task.id}`)}
  onAddTask={() => setCreateOpen(true)}
/>

All data flows in via tasks: TaskRecord[]. No fetching inside the block.

task-detail

Two-column detail page for a single task. Left column: header with inline status/priority selectors, subtask list, DAG visualization, and trigger configuration. Right column: live activity feed, run history timeline, and properties panel.

import { TaskDetailPage } from "@/components/task-detail/task-detail-page";

<TaskDetailPage
  task={task}
  childTasks={childTasks}
  sessions={sessions}
  onRunNow={() => triggerTask(task.id)}
  onStatusChange={(status) => updateTask(task.id, { status })}
  onAddSubtask={(title) => createTask({ title, parent_task_id: task.id })}
  onTaskClick={(t) => router.push(`/tasks/${t.id}`)}
  onRetrySession={(id) => retrySession(id)}
  onCancelSession={(id) => cancelSession(id)}
/>

The DAG view (@xyflow/react + dagre) renders automatically when child tasks have depends_on edges. It only appears when dependencies exist — no configuration required.

kanban (primitives)

Entity-agnostic drag-and-drop primitives used internally by task-hub. Use directly when building custom boards:

import {
  KanbanContainer,
  KanbanColumn,
  KanbanDragContext,
} from "@/components/kanban/kanban-container";

<KanbanDragContext onDragEnd={handleDragEnd} renderOverlay={renderOverlay}>
  <KanbanContainer>
    {columns.map((col) => (
      <KanbanColumn key={col.id} id={col.id} title={col.title} count={col.items.length}>
        {/* your draggable cards */}
      </KanbanColumn>
    ))}
  </KanbanContainer>
</KanbanDragContext>