Sprinter Docs

Tasks — The Missing Primitive

Introduce Tasks as the 6th platform primitive — a first-class, visible unit of work assignable to agents or humans, with parent/child hierarchy, event triggers, and completion criteria. Consolidates extraction configs, status triggers, field actions, and automation entities into one concept. Provides a unified backlog, per-user/agent workload views, and full auditability.

Tasks — The Missing Primitive

For the implementing agent: this spec is self-contained. Read it top-to-bottom, open the files it cites, and implement without needing additional context. Everything you need to understand the existing system is in S2 Background. Everything you need to build is in S4 Design. If anything here disagrees with the code you find, trust the code and flag the discrepancy.

1. Summary

The Sprinter Platform has five of its six core primitives as first-class concepts: Record (entities), Agent (agents), Work (workflow_runs), Message (messages/chats), Context (tenants/roles). Task is the missing 6th primitive — the unit of work that makes the system action-oriented.

Today, "what work should happen" is scattered across field config JSON (FieldConfig.extraction), entity type config arrays (statusTriggers), field config arrays (FieldConfig.actions), a special automation entity type (content.steps), and hardcoded Inngest event handlers. All of these are the same thing: a unit of work, assigned to an agent or human, triggered by an event or on-demand, that produces a result.

This spec introduces Tasks as a first-class table and module. A Task is like a task in Asana — but for both humans and agents, interchangeably. Users and agents share the same work queue, the same assignment model, the same visibility. Tasks are:

  • Visible — shown on every entity and in per-user/agent workload views, not hidden in background jobs
  • Assignable — to a specific agent, a specific human, or left unassigned for anyone to claim
  • Nestable — parent/child hierarchy (like Todoist) for decomposition into subtasks
  • Agent-native — high-level instructions (SOPs), not fragile step-by-step configs. Agents figure out HOW.
  • Claimable — agents pick up unassigned tasks during heartbeat; humans pick them up from their backlog
  • Auditable — every run is a chat conversation you can inspect, follow up on, or hand off
  • Standalone or attached — tasks can be entity-scoped, type-scoped, or standalone backlog items

The key decoupling:

PrimitiveDefinesTable
Fields (on entity types)What data exists — schema, types, validationentity_types.config.fields
TasksWhat work happens — instructions, assignee, trigger, outputtasks
RunsWhat actually happened — execution historyworkflow_runs + workflow_node_runs

Different tenants can use the same entity type with different tasks. Same fields, different extraction logic. Same schema, different automations.

What this unlocks:

  • "Generate social posts" button on Content Piece entities, with subtasks per platform
  • "Extract fields" as a visible, toggleable system task — not invisible background magic
  • "Email prospects when published" on a specific entity, not hardcoded on the type
  • A task backlog for the whole tenant — what needs doing, who's doing what
  • Per-user and per-agent workload views — "what's on Tyler's plate?" / "what's the analyst agent working on?"
  • Agents claim and complete available tasks during heartbeat scans
  • Failed tasks go back to the queue with run history, so the next agent/human avoids the same mistakes
  • Every task run is a chat conversation — inspect, follow up, hand off
  • "Save as task" from chat — extract an agent's task breakdown into reusable task templates
  • One admin surface for ALL work definitions on an entity type
  • Standalone tasks not attached to any entity — "set up CRM integration", "review Q4 pipeline"

What this consolidates (deprecation targets):

Current mechanismLines of codeReplaced by
FieldConfig.extraction (instructions, agentSlug, sources, dependsOn)~40 fields across types.ts + compile.ts + extract-field.tsTask rows with output_type: "field"
EntityTypeConfig.statusTriggers[]~30 lines in types.ts + handler codeTasks with trigger_type: "field_changed"
FieldConfig.actions[] (on_populate, on_approve, on_reject)~20 lines in types.ts + field-action.ts inngest fnTasks with field lifecycle triggers
Automation entity type + 5 inngest functions~800 lines across automation-*.ts + trigger-automation-workflow.tsTasks on source entity types + 2 generic inngest fns
Hardcoded extraction trigger in entity-extraction.ts~100 linesSystem "Extract fields" task
Implicit heartbeat attention scanning (goals + tasks + nodes)~200 lines in attention-context.tsSingle query: "what tasks need doing?"
agent-task / agent-goal entity types~100 lines in agent-entity-types.ts + attention-context.tsTasks table replaces these entity types for work tracking

Non-goals (deferred):

  • Auto-migrating existing automation entities to tasks (follow-up script)
  • Removing FieldConfig.extraction (deprecated, reads fall back to tasks, writes go to tasks)
  • Visual task/workflow builder (config-driven is sufficient for v1)
  • A2A protocol adapter for external agents claiming tasks (future, but the model supports it)
  • "Save as task" from chat (future — the model supports it, UI is deferred)
  • Full Kanban/sprint board views for tasks (standard entity views suffice for v1)

2. Background — what exists today

2a. The six primitives

From the project's architecture vision (documents/NORTH-STAR.md, memory: project_six_primitives.md):

Six architecture primitives: Record, Agent, Action, Work, Message, Context — everything else derived.

Five are implemented. Action is not. This spec closes the gap.

2b. Extraction flow (what Actions replace)

Extraction is currently defined on fields and executed invisibly:

  1. Definition: FieldConfig.extraction on entity type config — instructions, agentSlug, sources, dependsOn, consensus, refinement, requiresApproval
  2. Compilation: compileEntityWorkflowDefinition() reads field configs, produces a DAG of WorkflowNodeDefinition objects
  3. Trigger: entity/created Inngest event → hardcoded handler checks for extractable fields
  4. Execution: runEntityWorkflow() seeds workflow_node_runs, claims nodes, dispatches agents
  5. Result: Agent output written to entity.content or entity_responses

The problem: none of this is visible. Users can't see what extraction is configured, can't disable it per type, can't re-run individual fields, can't see run history. It "just happens" in the background.

2c. Automation entity type

Automations are defined as a special entity type with workflow steps in content.steps:

content: {
  status: "active" | "paused",
  trigger_type: "manual" | "cron" | "entity_created" | "field_changed",
  trigger_config: { schedule?, field?, to_value? },
  steps: [{ order, agent_slug, prompt, for_each? }]
}

Five dedicated Inngest functions handle automations: automationCronScan, automationRun, automationEntityCreatedTrigger, automationFieldChangedTrigger, fieldAction.

The problem: automations are disconnected from the entities they affect. A "Generate social posts" automation lives as a separate entity, not on the Content Piece entity type where it belongs.

2d. Status triggers and field actions

EntityTypeConfig.statusTriggers[] and FieldConfig.actions[] are small config arrays buried in JSON. They handle reactive work (field changed → do something) but are invisible, hard to discover, and don't integrate with the workflow engine.

2e. Agent heartbeat and attention

buildAttentionSnapshot() scans three separate sources: workflow_node_runs (pending nodes), agent-goal entities, and agent-task entities. It builds a markdown summary injected into the heartbeat prompt. This works but is fragmented — three different query patterns for the same concept: "what work needs doing?"

2f. Workflow execution layer (stays unchanged)

workflow_runs and workflow_node_runs are the execution layer. They track what happened when work ran. These stay — Actions don't replace execution, they replace definition. An Action defines what work should happen; a workflow_run records what happened when it did.

2g. Industry alignment

PlatformUnit of workDefinition styleHITL pattern
A2A ProtocolTask (stateful, with lifecycle)Messages over JSON-RPCinput_required state → resume
Anthropic Managed AgentsOutcome (description + rubric + grader)Natural language + rubricrequires_action → tool confirmation
CrewAITask (declarative, NL + output schema)Natural language + Pydantichuman_input=True gate
Microsoft Agent FrameworkProgress Ledger (facts + plan + assessment)NL instructions + YAMLTool approval + plan review
OpenAI Agents SDKRun (agent loop turn)NL instructions on agentsTool approval + RunState resume
Sprinter TasksTask (nestable, claimable, assignable to agents or humans)NL instructions (SOPs)Human assignment + claim model

Our Tasks align most closely with A2A's Task model (stateful, lifecycle-driven) combined with CrewAI's declarative style (natural language definition + output contract) and Anthropic's Outcome pattern (completion criteria evaluated by a grader). The parent/child hierarchy and unified agent/human assignment are our differentiators — no platform has a Todoist-style nested task decomposition where agents and humans are interchangeable assignees.

3. Design

3a. Table: tasks

CREATE TABLE tasks (
  id              uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id       uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,

  -- Scope: where this task applies (all nullable — standalone tasks have only tenant_id)
  entity_type_id  uuid REFERENCES entity_types(id) ON DELETE CASCADE,
  entity_id       uuid REFERENCES entities(id) ON DELETE CASCADE,

  -- Hierarchy
  parent_id       uuid REFERENCES tasks(id) ON DELETE CASCADE,
  depends_on      text[] NOT NULL DEFAULT '{}',
  sort_order      integer NOT NULL DEFAULT 0,

  -- Definition
  name            text NOT NULL,
  slug            text NOT NULL,
  description     text,
  instructions    text,
  completion_criteria text,

  -- Assignment: agent, human, or unassigned (claimable by anyone)
  agent_slug      text,                   -- assigned agent (null = not agent-assigned)
  assigned_to     uuid REFERENCES profiles(id),  -- assigned human (null = not human-assigned)
  -- Both null = unassigned, claimable by any agent or human with access

  -- Trigger
  trigger_type    text NOT NULL DEFAULT 'manual'
                  CHECK (trigger_type IN (
                    'manual', 'entity_created', 'entity_updated',
                    'field_changed', 'cron', 'webhook'
                  )),
  trigger_config  jsonb NOT NULL DEFAULT '{}',

  -- Output
  output_type     text
                  CHECK (output_type IS NULL OR output_type IN (
                    'field', 'fields', 'entity', 'entities',
                    'relation-entity', 'document', 'status', 'none'
                  )),
  output_config   jsonb NOT NULL DEFAULT '{}',

  -- State
  status          text NOT NULL DEFAULT 'active'
                  CHECK (status IN ('draft', 'active', 'paused', 'disabled', 'completed')),
  is_system       boolean NOT NULL DEFAULT false,

  -- Runtime metadata (updated after runs)
  last_run_at     timestamptz,
  last_run_status text,
  run_count       integer NOT NULL DEFAULT 0,

  -- Config
  metadata        jsonb NOT NULL DEFAULT '{}',

  -- Audit
  created_by      uuid REFERENCES profiles(id),
  created_at      timestamptz NOT NULL DEFAULT now(),
  updated_at      timestamptz NOT NULL DEFAULT now(),

  -- No scope_check constraint — tasks can be standalone (just tenant_id)
  -- Standalone: backlog items like "set up CRM integration"
  -- Type-level: templates like "extract fields" (inherited by all entities)
  -- Entity-level: specific work like "email prospects about this article"
  -- Child: subtask with parent_id

  CONSTRAINT tasks_slug_unique
    UNIQUE NULLS NOT DISTINCT (tenant_id, entity_type_id, entity_id, parent_id, slug)
);

Column details:

ColumnPurposeExample
entity_type_idType-level task template (inherited by all entities of this type)"Extract fields" on Opportunity type
entity_idEntity-level task (specific to one entity)"Email prospects about this article" on one blog post
parent_idParent task for nesting (Todoist model)Subtask "Extract summary" under parent "Extract fields"
depends_onSibling task slugs that must complete first["extract-summary", "extract-revenue"]
instructionsThe SOP/prompt — NL instructions for the agent or human"Search for the company's annual revenue using SEC filings and web search."
completion_criteriaHow to determine success (like Anthropic Outcomes)"The revenue field contains a numeric value with a cited source."
agent_slugAssigned agent (null = not agent-assigned)"analyst"
assigned_toAssigned human (null = not human-assigned)UUID of a user
(both null)Unassigned — claimable by any agent or human with accessBacklog item
trigger_typeWhen to run. Subtasks inherit parent trigger. Manual always available."entity_created"
trigger_configTrigger-specific conditions{ field: "status", to: "published" }
output_typeWhat the task produces (reuses WorkflowOutputType)"field"
output_configOutput details{ fieldNames: ["summary"], sources: ["linked-documents", "web"] }
statusTask lifecycle: draft → active → completed (one-off) or stays active (recurring)"completed" for done one-off tasks
metadataExtension point: maxSteps, inputSchema, consensus, refinement, etc.{ maxSteps: 5, sources: ["web"] }

Indexes:

CREATE INDEX idx_tasks_tenant ON tasks(tenant_id);
CREATE INDEX idx_tasks_type ON tasks(tenant_id, entity_type_id)
  WHERE entity_type_id IS NOT NULL;
CREATE INDEX idx_tasks_entity ON tasks(tenant_id, entity_id)
  WHERE entity_id IS NOT NULL;
CREATE INDEX idx_tasks_parent ON tasks(parent_id)
  WHERE parent_id IS NOT NULL;
CREATE INDEX idx_tasks_trigger ON tasks(tenant_id, trigger_type, status)
  WHERE status = 'active';
CREATE INDEX idx_tasks_cron ON tasks(tenant_id)
  WHERE trigger_type = 'cron' AND status = 'active';
CREATE INDEX idx_tasks_agent ON tasks(tenant_id, agent_slug)
  WHERE agent_slug IS NOT NULL AND status = 'active';
CREATE INDEX idx_tasks_user ON tasks(tenant_id, assigned_to)
  WHERE assigned_to IS NOT NULL AND status IN ('active', 'draft');
CREATE INDEX idx_tasks_backlog ON tasks(tenant_id, status)
  WHERE agent_slug IS NULL AND assigned_to IS NULL AND status = 'active';

RLS: Tenant members read all tasks. Editors+ create/update. System tasks non-deletable. Service role full access.

Add task_id to workflow_node_runs so every execution links back to the task that defined it:

ALTER TABLE workflow_node_runs
  ADD COLUMN task_id uuid REFERENCES tasks(id) ON DELETE SET NULL;

CREATE INDEX idx_workflow_node_runs_task ON workflow_node_runs(task_id)
  WHERE task_id IS NOT NULL;

And ensure every workflow_node_run has a chat_id for inspectable run history:

-- chat_id may already exist in metadata for some runs; promote to column
ALTER TABLE workflow_node_runs
  ADD COLUMN IF NOT EXISTS chat_id uuid REFERENCES chats(id) ON DELETE SET NULL;

3c. TypeScript types

// features/tasks/types.ts

export const TASK_TRIGGER_TYPES = [
  "manual",
  "entity_created",
  "entity_updated",
  "field_changed",
  "cron",
  "webhook",
] as const;
export type TaskTriggerType = (typeof TASK_TRIGGER_TYPES)[number];

export const TASK_STATUSES = ["draft", "active", "paused", "disabled", "completed"] as const;
export type TaskStatus = (typeof TASK_STATUSES)[number];

export interface TaskRecord {
  id: string;
  tenant_id: string;
  entity_type_id: string | null;
  entity_id: string | null;
  parent_id: string | null;
  depends_on: string[];
  sort_order: number;
  name: string;
  slug: string;
  description: string | null;
  instructions: string | null;
  completion_criteria: string | null;
  agent_slug: string | null;
  assigned_to: string | null;       // user UUID for human assignment
  trigger_type: TaskTriggerType;
  trigger_config: Record<string, unknown>;
  output_type: WorkflowOutputType | null;
  output_config: TaskOutputConfig;
  status: TaskStatus;
  is_system: boolean;
  last_run_at: string | null;
  last_run_status: string | null;
  run_count: number;
  metadata: TaskMetadata;
  created_by: string | null;
  created_at: string;
  updated_at: string;
}

export interface TaskOutputConfig {
  /** Field names this action writes to */
  fieldNames?: string[];
  /** Entity type slug for entity/relation-entity outputs */
  entityTypeSlug?: string;
  /** Extraction sources for agent context */
  sources?: ExtractionSource[];
}

export interface TaskMetadata {
  /** Max agent loop iterations (default: 10) */
  maxSteps?: number;
  /** JSON Schema for user input before manual trigger */
  inputSchema?: Record<string, unknown>;
  /** Consensus config (multi-agent) */
  consensus?: {
    agentSlugs: string[];
    strategy: "majority" | "unanimous" | "best-confidence";
  };
  /** Refinement loop config */
  refinement?: {
    enabled: boolean;
    judgeAgent?: string;
    maxIterations?: number;
    qualityThreshold?: number;
    refinementPrompt?: string;
  };
  /** For system extraction actions: original field config reference */
  fieldKey?: string;
  /** Requires human approval before output is promoted */
  requiresApproval?: boolean;
}

/** Resolved task tree for an entity — type-level + entity-level merged */
export interface TaskTree {
  roots: TaskNode[];
}

export interface TaskNode {
  task: TaskRecord;
  children: TaskNode[];
  lastRun?: {
    id: string;
    status: WorkflowNodeStatus;
    chat_id: string | null;
    completed_at: string | null;
  };
}

3d. Resolution: building the task tree

// features/tasks/server/resolve.ts

/**
 * Resolve all tasks for an entity, merging type-level and entity-level.
 * Returns a tree structure (parent/child) with last run status.
 */
export async function resolveTaskTree(
  tenantId: string,
  entityTypeId: string,
  entityId?: string,
): Promise<TaskTree> {
  // 1. Fetch all tasks for this entity type (where entity_id IS NULL)
  // 2. If entityId, also fetch entity-level tasks
  // 3. Entity-level tasks override type-level by slug
  // 4. Build tree from parent_id relationships
  // 5. For each leaf/node, fetch last workflow_node_run with task_id = task.id
  // 6. Return TaskTree with roots[] containing nested TaskNode[]
}

/**
 * Get claimable tasks for an agent — used by heartbeat.
 * Returns tasks where agent_slug matches or is null (unassigned).
 */
export async function getClaimableTasks(
  tenantId: string,
  agentSlug: string,
): Promise<TaskRecord[]> {
  // 1. Query tasks WHERE status = 'active'
  //    AND (agent_slug = agentSlug OR (agent_slug IS NULL AND assigned_to IS NULL))
  // 2. Check for pending workflow_node_runs linked to these tasks
  // 3. Check for tasks that should have been triggered but haven't
  // 4. Return sorted by priority (sort_order, then depends_on satisfaction)
}

/**
 * Get all tasks assigned to a user — used by task backlog views.
 */
export async function getUserTasks(
  tenantId: string,
  userId: string,
): Promise<TaskRecord[]> {
  // Query tasks WHERE assigned_to = userId AND status IN ('active', 'draft')
  // Include last run status
  // Order by sort_order, then created_at
}

/**
 * Get tenant-wide task backlog — unassigned tasks available to claim.
 */
export async function getTaskBacklog(
  tenantId: string,
): Promise<TaskRecord[]> {
  // Query tasks WHERE agent_slug IS NULL AND assigned_to IS NULL
  //   AND status = 'active'
  // Order by sort_order, then created_at
}

3e. Execution: triggering a task

When a task is triggered (manually, by event, or by heartbeat claim):

Task triggered

Is this a parent task with subtasks?
  ├─ YES: Create workflow_run, seed workflow_node_runs per subtask
  │       (respecting depends_on for execution order)
  │       Execute using existing runEntityWorkflow() patterns
  │       Each subtask node gets its own chat for the conversation

  └─ NO (leaf task): Create workflow_run + single workflow_node_run
                     Create chat for the run conversation
                     Execute agent with task.instructions (or wait for human)
                     Agent has entity context + tools

                     On completion: evaluate completion_criteria
                     ├─ Met: mark completed, write output per output_type
                     │       For one-off tasks: set task.status = 'completed'
                     └─ Not met: mark failed, task remains claimable
                         Run history preserved, linked via chat_id
                         Next claim gets context: "Previous attempt failed: [reason]"
// features/tasks/server/trigger.ts

export interface TriggerTaskInput {
  taskId: string;
  entityId?: string;          // required for type-level tasks, optional for standalone
  tenantId: string;
  triggeredBy: "manual" | "event" | "cron" | "heartbeat";
  userInput?: Record<string, unknown>;  // from inputSchema modal
}

export interface TriggerTaskResult {
  runId: string;
  chatId: string;             // every run is a chat conversation
  status: WorkflowRunStatus;
  nodeCount: number;
}

export async function triggerTask(
  input: TriggerTaskInput
): Promise<TriggerTaskResult> {
  // 1. Load task (+ entity + entity type if entity-scoped)
  // 2. If task has subtasks: load children, resolve depends_on
  // 3. Create workflow_run linked to task
  // 4. Create chat for the run (title: "{task.name}" or "{task.name} on {entity.title}")
  // 5. For each executable node (task or subtasks):
  //    a. Create workflow_node_run with task_id + chat_id
  //    b. Set status based on dependency satisfaction
  //    c. For human-assigned subtasks: set to waiting_human
  // 6. Execute using adapted runEntityWorkflow() patterns:
  //    - Resolve agent (task.agent_slug → parent → entity type default)
  //    - Build prompt from task.instructions with {field} substitution
  //    - Inject entity context + previous run history (if retry)
  //    - Execute via executeAgentSync() (background) or executeAgent() (manual)
  //    - Record messages to the run's chat
  // 7. On completion: evaluate completion_criteria if present
  //    - If criteria met: mark completed, write output
  //    - If criteria not met: mark failed with reason
  // 8. Update task.last_run_at, last_run_status, run_count
  // 9. For one-off tasks (no trigger_type event): set task.status = 'completed'
  // 10. Return result with chatId for user to inspect
}

3f. Chat as the run log

Every task run creates a chat. This is the run's complete record:

  • Agent messages — what the agent did, tool calls, reasoning
  • Tool results — entity creation, field updates, web searches
  • Multi-agent conversation — when delegation happens, the delegated agent's work appears as messages
  • Failure context — if the run fails, the error and context are in the chat
  • Human follow-up — users can send messages to the chat to continue or fix the work

When a previously-failed task is re-claimed, the new agent gets the previous run's chat as context: "Previous attempt by [agent] failed. Here's what happened: [chat link]. Avoid the same approach."

This aligns with Anthropic Managed Agents where sessions are long-lived conversations that include tool use, and users can steer the agent mid-execution.

3g. System tasks: extraction

When an entity type has fields with extraction configurations, the system auto-creates tasks:

// features/tasks/server/system-tasks.ts

export async function syncSystemTasks(
  tenantId: string,
  entityTypeId: string,
  fieldConfigs: Record<string, FieldConfig>,
  defaultAgentSlug?: string,
): Promise<void> {
  // 1. Upsert parent task:
  //    name: "Extract fields"
  //    slug: "extraction"
  //    trigger_type: "entity_created"
  //    is_system: true
  //
  // 2. For each field with extraction config:
  //    Upsert child task:
  //    name: "Extract {field.label || fieldKey}"
  //    slug: "extract-{fieldKey}"
  //    parent_id: extraction parent
  //    instructions: fieldConfig.extraction.instructions
  //    agent_slug: fieldConfig.extraction.agentSlug || defaultAgentSlug
  //    output_type: "field"
  //    output_config: { fieldNames: [fieldKey], sources: extraction.sources }
  //    depends_on: extraction.dependsOn (mapped to "extract-{dep}" slugs)
  //    metadata: { maxSteps, consensus, refinement, requiresApproval, fieldKey }
  //    is_system: true
  //
  // 3. For fields with humanInput: true:
  //    Upsert child task:
  //    slug: "input-{fieldKey}"
  //    assigned_to: null, agent_slug: null (human-assigned, appears in backlog)
  //    is_system: true
  //
  // 4. Remove system tasks for fields that no longer have extraction config
}

Migration path: On first access (or via a one-time migration script), existing FieldConfig.extraction entries are synced to task rows. The FieldConfig.extraction property is marked @deprecated. New extraction config should be added as tasks. The DAG compiler is updated to read from tasks.

The existing DAG compiler (compileEntityWorkflowDefinition) gets a new entry point that reads tasks instead of field configs:

// features/workflows/compile.ts (updated)

export function compileTasksToWorkflow(
  tasks: TaskRecord[],
  parentTask: TaskRecord,
): WorkflowDefinition {
  // Convert task tree to WorkflowNodeDefinition[]
  // Each subtask → one node
  // depends_on → converted to node key dependencies
  // Output contracts from task.output_type + output_config
  // Consensus/refinement from task.metadata
}

3h. Heartbeat integration: claim model

The heartbeat attention snapshot becomes simpler — one concept instead of three:

// features/agents/lib/attention-context.ts (updated)

export async function buildAttentionSnapshot(admin, params) {
  // BEFORE: scan workflow_node_runs + agent-goal entities + agent-task entities
  // AFTER: scan tasks table

  // 1. Get claimable tasks for this agent
  const claimable = await getClaimableTasks(params.tenantId, params.agentSlug);

  // 2. Get in-progress runs for this agent
  const inProgress = await getInProgressRuns(params.tenantId, params.agentSlug);

  // 3. Get recently failed runs (available for retry)
  const failed = await getFailedRuns(params.tenantId, params.agentSlug);

  // 4. Build attention context:
  //    "Tasks available to claim:"
  //    - [Extract fields] on "Acme Corp" (Opportunity) — triggered by entity creation
  //    - [Generate social posts] on "Q4 Report" (Content Piece) — manual, unclaimed
  //
  //    "Tasks in progress:"
  //    - [Extract revenue] on "Acme Corp" — running (started 2 min ago)
  //
  //    "Tasks that failed (available for retry):"
  //    - [Extract website] on "Beta Inc" — failed 1 hour ago
  //      Previous attempt: could not find company website via web search
  //      Chat: /chat/abc123

  return { context, claimableCount, inProgressCount, failedCount };
}

The heartbeat agent sees its work queue as a clear list of actions with full context. No more scanning three different sources. One concept: "what actions need my attention?"

3i. Inngest consolidation

New: taskDispatch — replaces 4 existing functions

// features/inngest/functions/task-dispatch.ts

export const taskDispatch = inngest.createFunction(
  { id: "task-dispatch", concurrency: { limit: 5 } },
  [
    { event: EVENT_NAMES.ENTITY_CREATED },
    { event: EVENT_NAMES.ENTITY_UPDATED },
  ],
  async ({ event, step }) => {
    // 1. Map event to trigger types:
    //    entity/created → ["entity_created"]
    //    entity/updated → ["entity_updated", "field_changed"]
    //
    // 2. Query tasks WHERE:
    //    tenant_id matches
    //    AND (entity_type_id matches OR entity_id matches)
    //    AND trigger_type IN (matched types)
    //    AND status = 'active'
    //
    // 3. For field_changed triggers: check trigger_config conditions
    //    against the event's changed fields
    //
    // 4. For each matching task: trigger via step.run()
    //    Reuse triggerTask() with triggeredBy: "event"
  }
);

New: taskCron — replaces automationCronScan

// features/inngest/functions/task-cron.ts

export const taskCron = inngest.createFunction(
  { id: "task-cron" },
  { cron: "*/1 * * * *" },
  async ({ step }) => {
    // 1. Query tasks WHERE trigger_type = 'cron' AND status = 'active'
    // 2. Check schedule match via shouldRunNow()
    // 3. For entity-level actions: trigger for that entity
    // 4. For type-level actions: trigger once with type context
  }
);

Modified: entityExtraction — checks for system action

// features/inngest/functions/entity-extraction.ts (modified)

// Before: always runs extraction if entity type has extractable fields
// After: checks if "extraction" system task exists and is active
//   If active → triggers via triggerTask()
//   If disabled/paused → skip (user has turned off extraction)
//   If no task exists → fall back to legacy behavior (transition period)

3j. Block type: tasks

// features/blocks/definitions/tasks.ts

{
  type: "tasks",
  meta: {
    name: "Tasks",
    description: "Shows available tasks, run status, and history for an entity",
    icon: "Zap",
    category: "interactive",
  },
  configSchema: z.object({
    showSystem: z.boolean().default(true),
    showRunHistory: z.boolean().default(true),
    showChildren: z.boolean().default(true),
    layout: z.enum(["cards", "compact"]).default("cards"),
  }),
  dataRequirement: "entity-single",
  resolve: async (config, context) => {
    const tree = await resolveTaskTree(
      context.tenantId, context.entityTypeId, context.entityId
    );
    return { tree, entity: context.entity };
  },
}

Rendering per TaskNode:

┌─ Task Card ────────────────────────────────────────────────┐
│ ⚡ Generate social posts            [Active] [▶ Run Now]   │
│ Create FB, IG, and LinkedIn drafts from this content piece │
│ Trigger: manual · Agent: content-agent                     │
│ ├── Create FB draft          ✓ completed (2h ago)          │
│ ├── Create IG draft          ✓ completed (2h ago)          │
│ ├── Create LinkedIn draft    ✗ failed (1h ago) [View run]  │
│ └── Request approval         ○ blocked (depends on above)  │
│                                     Assigned to: Tyler      │
│                                                             │
│ Last run: 2 hours ago · Partial (3/4 completed)             │
│ [View conversation] [Re-run failed]                         │
└─────────────────────────────────────────────────────────────┘

Key interactions:

  • Run Now — triggers the task (opens input modal if metadata.inputSchema is set)
  • View conversation — opens the run's chat to see exactly what happened
  • Re-run failed — re-triggers only failed subtasks, with previous run context
  • Expand children — shows subtasks with individual status and assignees
  • Add task — inline editor to add entity-level tasks
  • Assign — assign to a user or agent (or leave unassigned for backlog)

3k. Agent tool: manageTasks

// features/tools/admin/task-tools.ts

{
  slug: "manageTasks",
  name: "Manage Tasks",
  description: "Create, update, delete, list, or trigger tasks on entity types, entities, or standalone",
  category: "admin",
  groups: ["admin"],
  inputSchema: z.object({
    action: z.enum(["create", "update", "delete", "list", "trigger"]),

    // For create/update:
    entityTypeSlug: z.string().optional(),
    entityId: z.string().optional(),
    parentSlug: z.string().optional(),
    name: z.string().optional(),
    slug: z.string().optional(),
    instructions: z.string().optional(),
    completionCriteria: z.string().optional(),
    agentSlug: z.string().optional(),
    assignedTo: z.string().optional(),     // user ID for human assignment
    triggerType: TaskTriggerTypeSchema.optional(),
    triggerConfig: z.record(z.unknown()).optional(),
    outputType: WorkflowOutputTypeSchema.optional(),
    outputConfig: z.record(z.unknown()).optional(),
    status: TaskStatusSchema.optional(),
    dependsOn: z.array(z.string()).optional(),

    // For trigger:
    taskId: z.string().optional(),
    taskSlug: z.string().optional(),
    targetEntityId: z.string().optional(),

    // For list:
    // entityTypeSlug, entityId, agentSlug, or userId filter

    // For update/delete:
    id: z.string().optional(),
  }),
}

This lets agents:

  • Create tasks on entity types ("add a 'Generate social posts' task to Content Piece")
  • Create subtasks ("break that down into FB, IG, and LinkedIn steps")
  • Trigger tasks ("run the extraction task on this entity")
  • List tasks ("what tasks are available for Opportunities?" / "what tasks are assigned to me?")
  • Assign tasks ("assign this task to Tyler" / "assign this to the analyst agent")
  • Create standalone backlog tasks ("create a task to set up CRM integration")

3l. Admin UI: Entity Types > Tasks tab

A new tab in entity type admin (alongside Schema, Fields, Criteria Sets):

┌─ Tasks Tab ────────────────────────────────────────────────┐
│                                                             │
│  System Tasks                                               │
│  ┌────────────────────────────────────────────────────────┐│
│  │ ⚡ Extract fields   [Active ▾]  [🔒 System]            ││
│  │   Trigger: entity_created (auto-run)                   ││
│  │   6 subtasks (5 agent, 1 human)                        ││
│  │   [Expand ▾]                                           ││
│  │     · Extract summary (analyst) — writes: summary      ││
│  │     · Extract revenue (analyst) — writes: revenue      ││
│  │     · Extract market (analyst) — writes: market_size   ││
│  │     · Provide strategy — assigned to: Tyler            ││
│  │     · Extract recommendation (analyst) — depends on ↑  ││
│  │     · Score opportunity (analyst) — depends on all     ││
│  └────────────────────────────────────────────────────────┘│
│                                                             │
│  Custom Tasks                                               │
│  ┌────────────────────────────────────────────────────────┐│
│  │ 📝 Generate social posts  [Active ▾]  [Edit] [Delete]  ││
│  │   Trigger: manual · Agent: content-agent               ││
│  │   3 subtasks                                            ││
│  └────────────────────────────────────────────────────────┘│
│                                                             │
│  [+ Add task]                                               │
└─────────────────────────────────────────────────────────────┘

3m. Workload views: per-user, per-agent, backlog

Tasks enable unified workload visibility across the tenant:

/tasks page — tenant-wide task board:

  • My Tasks — tasks where assigned_to = current user (human tasks awaiting input)
  • Backlog — unassigned tasks (agent_slug IS NULL AND assigned_to IS NULL)
  • By Agent — tasks grouped by agent_slug (what each agent is working on)
  • By Entity Type — tasks grouped by entity type (what work is defined per type)
  • Completed — recently completed tasks

Agent admin page — "Tasks" section showing all tasks assigned to that agent, their status, and run history.

Dashboard integration — task counts and status in dashboard widgets: "12 tasks in progress, 3 waiting for human input, 5 in backlog."

For v1, the /tasks page can use standard entity list patterns (data-table, kanban by status). Rich Kanban/sprint board views are deferred.

3n. API routes

POST   /api/tasks                — Create task
GET    /api/tasks                — List tasks (query: entityTypeId, entityId, parentId, agentSlug, assignedTo, status)
PATCH  /api/tasks/[id]           — Update task
DELETE /api/tasks/[id]           — Delete task (blocks system tasks)
POST   /api/tasks/[id]/trigger   — Trigger task (creates run, returns chatId)
GET    /api/tasks/[id]/runs      — List runs for a task

All routes: Zod-validated, tenant-scoped, editor+ for mutations, member+ for trigger and list.

3o. A2A / external agent alignment

The tasks model maps naturally to the A2A protocol for future external agent integration:

Tasks conceptA2A equivalentIntegration path
Task trigger → runmessage/send → Task createdTask trigger sends A2A message to external agent
Run status updatesTaskStatus (working/completed/failed)External agent reports status back via A2A
Run chat messagesTask Message historyA2A messages appear as chat messages in the run
Task outputTask ArtifactsExternal agent returns artifacts mapped to output_config
assigned_to (human)TaskState input_requiredPause/resume pattern identical
completion_criteriaOutcome rubric (Anthropic pattern)Evaluator checks criteria, marks complete or failed
Unassigned taskTask in submitted stateExternal agent can claim via A2A

External agents (via agent_connections) can be assigned to tasks just like internal agents. The agent_slug references an agent record that may have an external connection. Execution routes through the existing delegation/connection system. The unified human/agent assignment model means an A2A external agent is just another assignee.

4. What gets simpler

4a. Removed/deprecated code

WhatSizeReplacementTimeline
FieldConfig.extraction property~60 lines in types.tsTask rows with output_type: "field"Deprecated v1, removed v2
EntityTypeConfig.statusTriggers~30 lines in types.ts + handlersTasks with field_changed triggerDeprecated v1, removed v2
FieldConfig.actions[]~20 lines in types.tsTasks with field lifecycle triggersDeprecated v1, removed v2
automationCronScan inngest fn~80 linestaskCronReplace in v1
automationEntityCreatedTrigger inngest fn~100 linestaskDispatchReplace in v1
automationFieldChangedTrigger inngest fn~100 linestaskDispatchReplace in v1
fieldAction inngest fn~80 linestaskDispatchReplace in v1
automationRun inngest fn~40 linestriggerTask()Replace in v1
trigger-automation-workflow.ts~300 linesfeatures/tasks/server/trigger.tsConsolidate in v1
features/entities/extraction/actions.ts~100 linesTasks with field lifecycle triggersDeprecated v1
features/entities/extraction/extract-field.ts~200 linesTask execution via trigger.tsConsolidate in v1
Hardcoded extraction trigger in entity-extraction.ts~100 linesSystem task checkSimplify in v1
Three-source attention snapshot~200 linesSingle tasks querySimplify in v1
agent-task / agent-goal entity types~100 linesTasks tableDeprecated v1
Total removed/simplified~1,500 lines

4b. Conceptual simplification

Before (7 concepts for "do work"):

  1. Field extraction configs (hidden in entity type JSON)
  2. Status triggers (hidden in entity type JSON)
  3. Field actions (hidden in field config JSON)
  4. Automation entity type (separate entity with steps)
  5. Hardcoded inngest handlers (invisible code)
  6. Heartbeat attention scanning (three query patterns)
  7. agent-task / agent-goal entity types (ad-hoc work tracking)

After (1 concept):

  1. Tasks — visible, assignable, nestable, claimable, for agents and humans alike

4c. What stays unchanged

  • workflow_runs + workflow_node_runs — execution layer, now with task_id FK
  • Agent resolver, tool system, runtime — unchanged
  • FieldConfig sans extraction — field configs still define label, displayType, relation, humanInput, statusMap
  • DAG compiler — updated to read from tasks instead of field configs, same algorithm
  • Chat/message system — unchanged, task runs create chats
  • Heartbeat schedule — still agent-level cron, now claims tasks instead of scanning three sources
  • json_schema on entity types — still defines data structure, unaffected

5. File inventory

New files

FilePurpose
supabase/migrations/20260410000000_tasks.sqlTable, indexes, RLS, triggers
supabase/migrations/20260410000001_workflow_node_runs_task_link.sqlAdd task_id + chat_id columns
features/tasks/types.tsTaskRecord, TaskTree, TaskNode, schemas
features/tasks/types.test.tsType validation tests
features/tasks/server/resolve.tsresolveTaskTree(), getClaimableTasks(), getUserTasks(), getTaskBacklog()
features/tasks/server/resolve.test.tsResolution + merge tests
features/tasks/server/trigger.tstriggerTask() execution
features/tasks/server/trigger.test.tsTrigger execution tests
features/tasks/server/system-tasks.tssyncSystemTasks() from field configs
features/tasks/server/system-tasks.test.tsSystem task sync tests
features/tasks/server/crud.tsCRUD server actions
features/tasks/server/crud.test.tsCRUD tests
features/tasks/components/task-list.tsxTask list for entity detail + backlog
features/tasks/components/task-card.tsxIndividual task card with subtasks
features/tasks/components/task-editor.tsxCreate/edit task sheet
features/tasks/components/task-run-history.tsxRun history panel with chat link
features/blocks/definitions/tasks.tsBlock definition + resolver
features/blocks/definitions/tasks.test.tsBlock tests
features/inngest/functions/task-dispatch.tsEvent-triggered task dispatcher
features/inngest/functions/task-cron.tsCron task scanner
features/tools/admin/task-tools.tsmanageTasks agent tool
features/tools/admin/task-tools.test.tsTool tests
app/api/tasks/route.tsCreate + list API
app/api/tasks/[id]/route.tsUpdate + delete API
app/api/tasks/[id]/trigger/route.tsManual trigger API
app/api/tasks/[id]/runs/route.tsRun history API
app/(app)/tasks/page.tsxTenant-wide task backlog page
content/docs/features/tasks.mdxFeature documentation

Modified files

FileChange
features/workflows/types.tsAdd task_id to WorkflowNodeRunRecord
features/workflows/compile.tsAdd compileTasksToWorkflow()
features/workflows/run-workflow.tsAccept task_id, link runs to tasks
features/agents/lib/attention-context.tsSimplify to single tasks query
features/inngest/functions/entity-extraction.tsCheck for system task before running
features/inngest/index.tsRegister new inngest functions
features/blocks/definition.tsRegister tasks block
features/tools/bootstrap.tsRegister task tools
features/entities/types.ts@deprecated on extraction, statusTriggers, actions
lib/supabase/database.types.tsRegenerated

6. Implementation order

Phase 1: Foundation (table + types + CRUD)

  1. Migration: tasks table + indexes + RLS
  2. Migration: workflow_node_runs add task_id + chat_id columns
  3. pnpm db:types
  4. Types: features/tasks/types.ts + tests
  5. CRUD: features/tasks/server/crud.ts + tests
  6. API routes: /api/tasks CRUD + tests
  7. Resolution: resolveTaskTree(), getClaimableTasks(), getUserTasks(), getTaskBacklog() + tests

Phase 2: System tasks + extraction bridge

  1. syncSystemTasks() — auto-create from FieldConfig.extraction + tests
  2. compileTasksToWorkflow() — compile task tree to WorkflowDefinition + tests
  3. Update entity-extraction.ts — check for system task, use task-based compilation
  4. Mark FieldConfig.extraction as @deprecated in types.ts

Phase 3: Execution + Inngest

  1. triggerTask() — execution function + chat creation + tests
  2. taskDispatch inngest function — replaces 4 automation functions
  3. taskCron inngest function — replaces automationCronScan
  4. Update heartbeat buildAttentionSnapshot() to use getClaimableTasks()

Phase 4: UI + block + tool

  1. tasks block definition + resolver + tests
  2. Task card component (with subtasks, run status, trigger button, assignee)
  3. Task editor component (create/edit sheet with agent/human assignment)
  4. Task run history component (with chat link)
  5. Entity type admin: "Tasks" tab
  6. Tenant-wide /tasks page (backlog, my tasks, by agent)
  7. manageTasks agent tool + tests
  8. Register block + tool in bootstrap

Phase 5: Deprecation + documentation

  1. Mark statusTriggers, FieldConfig.actions as @deprecated
  2. Update entity detail default views to include tasks block
  3. Feature documentation: content/docs/features/tasks.mdx
  4. Changelog entry

7. Acceptance criteria

  • tasks table created with RLS, indexes, parent_id self-reference, no scope_check
  • workflow_node_runs has task_id + chat_id columns
  • Type-level tasks: admin can CRUD via UI and API
  • Entity-level tasks: users can add via block editor and API
  • Standalone tasks: backlog items with only tenant_id
  • Human assignment via assigned_to, agent assignment via agent_slug, unassigned = claimable
  • Parent/child: tasks nest with depends_on ordering
  • System extraction tasks auto-created from FieldConfig.extraction
  • Extraction runs through task-based compilation (with legacy fallback)
  • Manual trigger: "Run Now" creates workflow_run + chat, returns chatId
  • Event triggers: entity_created, field_changed fire matching tasks
  • Cron triggers: scheduled tasks execute on schedule
  • Heartbeat claims: agents see claimable tasks in attention snapshot
  • Failed tasks: go back to claimable with run history preserved
  • Every run has a chat: viewable, follow-up-able
  • Completion criteria: evaluated after run, determines success/failure
  • One-off tasks set to 'completed' when done
  • Agent tool: manageTasks supports create, list, trigger, assign
  • Block: tasks renders on entity detail with full tree + status + assignees
  • Admin tab: "Tasks" on entity type admin page
  • /tasks page with backlog, my tasks, by agent views
  • All new files have co-located tests
  • pnpm typecheck && pnpm test && pnpm build pass
  • Existing extraction continues to work during transition

8. UI Components

This section defines all UI components. These are NOT deferred — they are part of v1 delivery, implemented after the core backend is solid.

8a. Task Card (features/tasks/components/task-card.tsx)

Renders a single task with expandable subtasks:

┌─ Task Card ────────────────────────────────────────────────┐
│ ⚡ Generate social posts            [Active] [▶ Run Now]   │
│ Create FB, IG, and LinkedIn drafts from this content piece │
│ Trigger: manual · Agent: content-agent                     │
│                                                             │
│ Subtasks:                                                   │
│ ├── Create FB draft          ✓ completed (2h ago)          │
│ ├── Create IG draft          ✓ completed (2h ago)          │
│ ├── Create LinkedIn draft    ✗ failed (1h ago) [View run]  │
│ │                            Assigned to: content-agent     │
│ └── Request approval         ○ blocked                     │
│                              Assigned to: Tyler             │
│                                                             │
│ Last run: 2 hours ago · Partial (3/4)                       │
│ [View conversation] [Re-run failed]                         │
└─────────────────────────────────────────────────────────────┘

Props: task: TaskNode, onTrigger: (taskId) => void, onViewRun: (chatId) => void

Key behaviors:

  • Subtasks collapsible (default expanded if any are in-progress/failed)
  • "Run Now" calls POST /api/tasks/[id]/trigger
  • "View conversation" opens the run's chat (navigates to /chat/[chatId])
  • "Re-run failed" re-triggers only failed subtasks
  • Status badges: draft (gray), active (blue), running (amber), completed (green), failed (red)
  • System tasks show lock icon, non-deletable
  • Uses shadcn Card, Badge, Button, Collapsible components

8b. Task List (features/tasks/components/task-list.tsx)

Renders the full task tree for an entity or a filtered list for backlog views:

┌─ Tasks ────────────────────────────────────────────────────┐
│  System                                                     │
│  [TaskCard: Extract fields]                                 │
│                                                             │
│  Custom                                                     │
│  [TaskCard: Generate social posts]                          │
│  [TaskCard: Email prospects on publish]                     │
│                                                             │
│  [+ Add task]                                               │
└─────────────────────────────────────────────────────────────┘

Props: tree: TaskTree, mode: "entity" | "backlog" | "agent" | "user", onAddTask: () => void

Modes:

  • entity — grouped by system/custom, shows full tree
  • backlog — flat list of unassigned tasks, no grouping
  • agent — tasks assigned to one agent, grouped by entity
  • user — tasks assigned to one user, grouped by entity

8c. Task Editor (features/tasks/components/task-editor.tsx)

Sheet overlay for creating/editing tasks. Uses shadcn Sheet, Form, Input, Textarea, Select, Switch.

Sections:

  1. Basics — name, slug (auto-generated), description
  2. Instructions — textarea for the SOP/prompt (the core content)
  3. Completion criteria — textarea for how to determine success
  4. Assignment — agent selector OR user selector OR "Unassigned (claimable)"
  5. Trigger — select trigger_type, conditional config fields:
    • manual: no extra config
    • entity_created: auto-run toggle
    • field_changed: field selector + from/to values
    • cron: schedule input + timezone selector
  6. Output — output_type selector, conditional config:
    • field: field name(s) selector, extraction sources
    • entity: target entity type slug
    • none: no config
  7. Advanced — max steps, input schema (JSON editor), requires approval toggle

8d. Task Run History (features/tasks/components/task-run-history.tsx)

Panel showing past runs for a task. Each run links to its chat.

┌─ Run History ──────────────────────────────────────────────┐
│  Run #3 · 2 hours ago · Partial (3/4)      [View chat →]  │
│  Run #2 · Yesterday · Failed               [View chat →]  │
│  Run #1 · 3 days ago · Completed           [View chat →]  │
└─────────────────────────────────────────────────────────────┘

Data source: workflow_runs WHERE metadata contains task_id, ordered by created_at DESC.

8e. Tasks Block (features/blocks/definitions/tasks.ts)

Already defined in S3j. The block component composes TaskList + TaskEditor. Block config:

  • showSystem: boolean — show system tasks (extraction)
  • showRunHistory: boolean — show run history on each card
  • layout: "cards" | "compact" — card view or compact list

8f. Entity Type Admin: Tasks Tab

New tab in app/(app)/admin/data-types/[slug]/entity-type-admin-client.tsx alongside Schema, Fields, Criteria Sets.

Renders TaskList in entity mode for the entity type, plus a "Sync system tasks" action that calls syncSystemTasks() to refresh extraction-derived tasks from current field configs.

8g. /tasks Page (app/(app)/tasks/page.tsx)

Tenant-wide task management page. Tabs:

  • My TasksgetUserTasks(tenantId, currentUserId) — tasks assigned to me
  • BackloggetTaskBacklog(tenantId) — unassigned tasks for claiming
  • By Agent — grouped by agent_slug, showing each agent's workload
  • All — all active tasks across the tenant

Each tab renders TaskList in the appropriate mode. Quick-create button for standalone tasks. Filter by status, entity type, trigger type.

8h. Agent Admin: Tasks Section

On the agent detail page (app/(app)/admin/agents/[id]/page.tsx), add a "Tasks" section below the existing run history. Shows all tasks where agent_slug matches this agent, with run status.

9. Consolidation Map — What Changes in Existing Code

This section is critical for implementation agents. It documents every file that changes and exactly what to do.

9a. Files to REPLACE (new tasks code handles this)

FileLinesWhat to do
features/inngest/functions/automation-cron.ts~91Replace with features/inngest/functions/task-cron.ts. Same cron pattern, queries tasks table instead of automation entities. Reuse shouldRunNow() from features/agents/heartbeat.ts.
features/inngest/functions/automation-run.ts~27Replace with direct triggerTask() call from task-dispatch.ts. This was a thin wrapper.
features/inngest/functions/automation-entity-trigger.ts~232Replace with features/inngest/functions/task-dispatch.ts. Listens for same events (entity/created, entity/updated), queries tasks table for matching triggers instead of automation entities.
features/workflows/trigger-automation-workflow.ts~533Reuse patterns in features/tasks/server/trigger.ts. Key functions to port: resolvePromptTemplate(), executeAutomationStep() execution pattern, fan-out concurrency. Do NOT delete — keep working during transition.
features/inngest/functions/field-action.ts~61Replace with task-dispatch handling field lifecycle events. Keep working during transition.

9b. Files to MODIFY (add tasks integration)

FileChange
features/workflows/types.tsAdd task_id: string | null to WorkflowNodeRunRecord. Add chat_id: string | null if not already present.
features/workflows/compile.tsAdd compileTasksToWorkflow(tasks: TaskRecord[], parent: TaskRecord): WorkflowDefinition — converts task tree into workflow nodes. Reuse existing node creation logic.
features/workflows/run-workflow.tsAccept optional task_id param. Pass to seedWorkflowNodeRuns() so new node_runs get task_id. After completion, update task runtime fields.
features/inngest/functions/entity-extraction.tsBefore existing extraction logic, query tasks table for system "extraction" task. If found and disabled, skip. If found and active, continue. If not found, legacy fallback. After completion, update task.last_run_at.
features/agents/lib/attention-context.tsAdd getClaimableTasks() to attention snapshot. Format as "Tasks available/in-progress/failed" sections. Keep existing goal/workflow scanning during transition (remove in follow-up).
features/inngest/client.tsAdd TASK_TRIGGERED: "task/triggered" to EVENT_NAMES with typed event data.
features/blocks/definitions/index.tsImport and register tasksDefinition from ./tasks.ts.
features/blocks/lib/block-metadata.tsAdd metadata entry for "tasks" block type (icon: "Zap", label: "Tasks", etc.).
features/tools/admin/index.tsAdd manageTasks: "entity_types.team.update" to ADMIN_TOOL_PERMISSIONS. Add ...getTaskAdminTools(ctx) to buildAdminToolSet().
features/tools/bootstrap.tsImport task tool definitions in bootstrapToolRegistry().
features/entities/types.tsAdd @deprecated JSDoc to: FieldConfig.extraction, FieldConfig.actions, EntityTypeConfig.statusTriggers, StatusTransitionTrigger. Each with "Use tasks instead" migration note.
features/views/lib/default-blocks.tsIn generateDefaultBlocks(), conditionally add a tasks block when the entity type has active tasks.

9c. Files to REUSE (call from tasks, don't modify)

FileWhat tasks call
features/workflows/run-workflow.tsrunEntityWorkflow() for system extraction tasks
features/agents/runtime.tsexecuteAgentSync() for custom task execution
features/agents/agent-resolver.tsresolveAgent() to resolve agent_slug to full agent
features/agents/lib/build-context.tsloadAgentPromptContext(), buildAgentSystemPrompt()
features/agents/lib/build-tools.tsbuildInternalAgentTools() for task agent's tool set
features/agents/heartbeat.tsshouldRunNow() for cron schedule matching
features/workflows/trigger-automation-workflow.tsresolvePromptTemplate() for {field} substitution
features/entities/extraction/dep-sort.tssortFieldsByDependency() for depends_on resolution
features/entities/extraction/consensus.tsConsensus execution for tasks with consensus config
features/chat/server/actions.tscreateChat(), saveMessage() for task run chats

9d. agent-task and agent-goal migration

The existing agent-task and agent-goal entity types are stored as regular entities. They are used by:

  • features/agents/lib/agent-entity-types.ts — defines the slugs
  • features/agents/lib/attention-context.ts — queries for goals/tasks in heartbeat
  • features/agents/lib/goal-context.ts — formats goals for agent prompts

Migration path:

  1. Phase 1 (this spec): Tasks table is the NEW way to define work. Heartbeat queries BOTH tasks table and legacy agent-task/agent-goal entities.
  2. Phase 2 (follow-up): Migrate existing agent-task/agent-goal entities to tasks table rows. Update heartbeat to query only tasks table. Deprecate the entity types.

Do NOT delete agent-task/agent-goal in this implementation. They coexist.

10. ADR Reference

Full architectural decision record at documents/ADR-001-tasks-primitive.md. Covers:

  • All alternatives considered and why they were rejected
  • Complete REUSE vs REPLACE decision matrix
  • Industry alignment (A2A, Anthropic, CrewAI, OpenAI, Microsoft)
  • Consequences and risks

Implementation agents should read the ADR if they need context on WHY a decision was made.

On this page

Tasks — The Missing Primitive1. Summary2. Background — what exists today2a. The six primitives2b. Extraction flow (what Actions replace)2c. Automation entity type2d. Status triggers and field actions2e. Agent heartbeat and attention2f. Workflow execution layer (stays unchanged)2g. Industry alignment3. Design3a. Table: tasks3b. Workflow execution link3c. TypeScript types3d. Resolution: building the task tree3e. Execution: triggering a task3f. Chat as the run log3g. System tasks: extraction3h. Heartbeat integration: claim model3i. Inngest consolidation3j. Block type: tasks3k. Agent tool: manageTasks3l. Admin UI: Entity Types > Tasks tab3m. Workload views: per-user, per-agent, backlog3n. API routes3o. A2A / external agent alignment4. What gets simpler4a. Removed/deprecated code4b. Conceptual simplification4c. What stays unchanged5. File inventoryNew filesModified files6. Implementation orderPhase 1: Foundation (table + types + CRUD)Phase 2: System tasks + extraction bridgePhase 3: Execution + InngestPhase 4: UI + block + toolPhase 5: Deprecation + documentation7. Acceptance criteria8. UI Components8a. Task Card (features/tasks/components/task-card.tsx)8b. Task List (features/tasks/components/task-list.tsx)8c. Task Editor (features/tasks/components/task-editor.tsx)8d. Task Run History (features/tasks/components/task-run-history.tsx)8e. Tasks Block (features/blocks/definitions/tasks.ts)8f. Entity Type Admin: Tasks Tab8g. /tasks Page (app/(app)/tasks/page.tsx)8h. Agent Admin: Tasks Section9. Consolidation Map — What Changes in Existing Code9a. Files to REPLACE (new tasks code handles this)9b. Files to MODIFY (add tasks integration)9c. Files to REUSE (call from tasks, don't modify)9d. agent-task and agent-goal migration10. ADR Reference