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:
| Primitive | Defines | Table |
|---|---|---|
| Fields (on entity types) | What data exists — schema, types, validation | entity_types.config.fields |
| Tasks | What work happens — instructions, assignee, trigger, output | tasks |
| Runs | What actually happened — execution history | workflow_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 mechanism | Lines of code | Replaced by |
|---|---|---|
FieldConfig.extraction (instructions, agentSlug, sources, dependsOn) | ~40 fields across types.ts + compile.ts + extract-field.ts | Task rows with output_type: "field" |
EntityTypeConfig.statusTriggers[] | ~30 lines in types.ts + handler code | Tasks with trigger_type: "field_changed" |
FieldConfig.actions[] (on_populate, on_approve, on_reject) | ~20 lines in types.ts + field-action.ts inngest fn | Tasks with field lifecycle triggers |
| Automation entity type + 5 inngest functions | ~800 lines across automation-*.ts + trigger-automation-workflow.ts | Tasks on source entity types + 2 generic inngest fns |
| Hardcoded extraction trigger in entity-extraction.ts | ~100 lines | System "Extract fields" task |
| Implicit heartbeat attention scanning (goals + tasks + nodes) | ~200 lines in attention-context.ts | Single query: "what tasks need doing?" |
agent-task / agent-goal entity types | ~100 lines in agent-entity-types.ts + attention-context.ts | Tasks 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:
- Definition:
FieldConfig.extractionon entity type config — instructions, agentSlug, sources, dependsOn, consensus, refinement, requiresApproval - Compilation:
compileEntityWorkflowDefinition()reads field configs, produces a DAG ofWorkflowNodeDefinitionobjects - Trigger:
entity/createdInngest event → hardcoded handler checks for extractable fields - Execution:
runEntityWorkflow()seedsworkflow_node_runs, claims nodes, dispatches agents - Result: Agent output written to
entity.contentorentity_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
| Platform | Unit of work | Definition style | HITL pattern |
|---|---|---|---|
| A2A Protocol | Task (stateful, with lifecycle) | Messages over JSON-RPC | input_required state → resume |
| Anthropic Managed Agents | Outcome (description + rubric + grader) | Natural language + rubric | requires_action → tool confirmation |
| CrewAI | Task (declarative, NL + output schema) | Natural language + Pydantic | human_input=True gate |
| Microsoft Agent Framework | Progress Ledger (facts + plan + assessment) | NL instructions + YAML | Tool approval + plan review |
| OpenAI Agents SDK | Run (agent loop turn) | NL instructions on agents | Tool approval + RunState resume |
| Sprinter Tasks | Task (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:
| Column | Purpose | Example |
|---|---|---|
entity_type_id | Type-level task template (inherited by all entities of this type) | "Extract fields" on Opportunity type |
entity_id | Entity-level task (specific to one entity) | "Email prospects about this article" on one blog post |
parent_id | Parent task for nesting (Todoist model) | Subtask "Extract summary" under parent "Extract fields" |
depends_on | Sibling task slugs that must complete first | ["extract-summary", "extract-revenue"] |
instructions | The SOP/prompt — NL instructions for the agent or human | "Search for the company's annual revenue using SEC filings and web search." |
completion_criteria | How to determine success (like Anthropic Outcomes) | "The revenue field contains a numeric value with a cited source." |
agent_slug | Assigned agent (null = not agent-assigned) | "analyst" |
assigned_to | Assigned human (null = not human-assigned) | UUID of a user |
| (both null) | Unassigned — claimable by any agent or human with access | Backlog item |
trigger_type | When to run. Subtasks inherit parent trigger. Manual always available. | "entity_created" |
trigger_config | Trigger-specific conditions | { field: "status", to: "published" } |
output_type | What the task produces (reuses WorkflowOutputType) | "field" |
output_config | Output details | { fieldNames: ["summary"], sources: ["linked-documents", "web"] } |
status | Task lifecycle: draft → active → completed (one-off) or stays active (recurring) | "completed" for done one-off tasks |
metadata | Extension 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.
3b. Workflow execution link
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.inputSchemais 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 taskAll 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 concept | A2A equivalent | Integration path |
|---|---|---|
| Task trigger → run | message/send → Task created | Task trigger sends A2A message to external agent |
| Run status updates | TaskStatus (working/completed/failed) | External agent reports status back via A2A |
| Run chat messages | Task Message history | A2A messages appear as chat messages in the run |
| Task output | Task Artifacts | External agent returns artifacts mapped to output_config |
assigned_to (human) | TaskState input_required | Pause/resume pattern identical |
completion_criteria | Outcome rubric (Anthropic pattern) | Evaluator checks criteria, marks complete or failed |
| Unassigned task | Task in submitted state | External 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
| What | Size | Replacement | Timeline |
|---|---|---|---|
FieldConfig.extraction property | ~60 lines in types.ts | Task rows with output_type: "field" | Deprecated v1, removed v2 |
EntityTypeConfig.statusTriggers | ~30 lines in types.ts + handlers | Tasks with field_changed trigger | Deprecated v1, removed v2 |
FieldConfig.actions[] | ~20 lines in types.ts | Tasks with field lifecycle triggers | Deprecated v1, removed v2 |
automationCronScan inngest fn | ~80 lines | taskCron | Replace in v1 |
automationEntityCreatedTrigger inngest fn | ~100 lines | taskDispatch | Replace in v1 |
automationFieldChangedTrigger inngest fn | ~100 lines | taskDispatch | Replace in v1 |
fieldAction inngest fn | ~80 lines | taskDispatch | Replace in v1 |
automationRun inngest fn | ~40 lines | triggerTask() | Replace in v1 |
trigger-automation-workflow.ts | ~300 lines | features/tasks/server/trigger.ts | Consolidate in v1 |
features/entities/extraction/actions.ts | ~100 lines | Tasks with field lifecycle triggers | Deprecated v1 |
features/entities/extraction/extract-field.ts | ~200 lines | Task execution via trigger.ts | Consolidate in v1 |
| Hardcoded extraction trigger in entity-extraction.ts | ~100 lines | System task check | Simplify in v1 |
| Three-source attention snapshot | ~200 lines | Single tasks query | Simplify in v1 |
agent-task / agent-goal entity types | ~100 lines | Tasks table | Deprecated v1 |
| Total removed/simplified | ~1,500 lines |
4b. Conceptual simplification
Before (7 concepts for "do work"):
- Field extraction configs (hidden in entity type JSON)
- Status triggers (hidden in entity type JSON)
- Field actions (hidden in field config JSON)
- Automation entity type (separate entity with steps)
- Hardcoded inngest handlers (invisible code)
- Heartbeat attention scanning (three query patterns)
- agent-task / agent-goal entity types (ad-hoc work tracking)
After (1 concept):
- Tasks — visible, assignable, nestable, claimable, for agents and humans alike
4c. What stays unchanged
workflow_runs+workflow_node_runs— execution layer, now withtask_idFK- Agent resolver, tool system, runtime — unchanged
FieldConfigsans 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_schemaon entity types — still defines data structure, unaffected
5. File inventory
New files
| File | Purpose |
|---|---|
supabase/migrations/20260410000000_tasks.sql | Table, indexes, RLS, triggers |
supabase/migrations/20260410000001_workflow_node_runs_task_link.sql | Add task_id + chat_id columns |
features/tasks/types.ts | TaskRecord, TaskTree, TaskNode, schemas |
features/tasks/types.test.ts | Type validation tests |
features/tasks/server/resolve.ts | resolveTaskTree(), getClaimableTasks(), getUserTasks(), getTaskBacklog() |
features/tasks/server/resolve.test.ts | Resolution + merge tests |
features/tasks/server/trigger.ts | triggerTask() execution |
features/tasks/server/trigger.test.ts | Trigger execution tests |
features/tasks/server/system-tasks.ts | syncSystemTasks() from field configs |
features/tasks/server/system-tasks.test.ts | System task sync tests |
features/tasks/server/crud.ts | CRUD server actions |
features/tasks/server/crud.test.ts | CRUD tests |
features/tasks/components/task-list.tsx | Task list for entity detail + backlog |
features/tasks/components/task-card.tsx | Individual task card with subtasks |
features/tasks/components/task-editor.tsx | Create/edit task sheet |
features/tasks/components/task-run-history.tsx | Run history panel with chat link |
features/blocks/definitions/tasks.ts | Block definition + resolver |
features/blocks/definitions/tasks.test.ts | Block tests |
features/inngest/functions/task-dispatch.ts | Event-triggered task dispatcher |
features/inngest/functions/task-cron.ts | Cron task scanner |
features/tools/admin/task-tools.ts | manageTasks agent tool |
features/tools/admin/task-tools.test.ts | Tool tests |
app/api/tasks/route.ts | Create + list API |
app/api/tasks/[id]/route.ts | Update + delete API |
app/api/tasks/[id]/trigger/route.ts | Manual trigger API |
app/api/tasks/[id]/runs/route.ts | Run history API |
app/(app)/tasks/page.tsx | Tenant-wide task backlog page |
content/docs/features/tasks.mdx | Feature documentation |
Modified files
| File | Change |
|---|---|
features/workflows/types.ts | Add task_id to WorkflowNodeRunRecord |
features/workflows/compile.ts | Add compileTasksToWorkflow() |
features/workflows/run-workflow.ts | Accept task_id, link runs to tasks |
features/agents/lib/attention-context.ts | Simplify to single tasks query |
features/inngest/functions/entity-extraction.ts | Check for system task before running |
features/inngest/index.ts | Register new inngest functions |
features/blocks/definition.ts | Register tasks block |
features/tools/bootstrap.ts | Register task tools |
features/entities/types.ts | @deprecated on extraction, statusTriggers, actions |
lib/supabase/database.types.ts | Regenerated |
6. Implementation order
Phase 1: Foundation (table + types + CRUD)
- Migration:
taskstable + indexes + RLS - Migration:
workflow_node_runsaddtask_id+chat_idcolumns pnpm db:types- Types:
features/tasks/types.ts+ tests - CRUD:
features/tasks/server/crud.ts+ tests - API routes:
/api/tasksCRUD + tests - Resolution:
resolveTaskTree(),getClaimableTasks(),getUserTasks(),getTaskBacklog()+ tests
Phase 2: System tasks + extraction bridge
syncSystemTasks()— auto-create from FieldConfig.extraction + testscompileTasksToWorkflow()— compile task tree to WorkflowDefinition + tests- Update
entity-extraction.ts— check for system task, use task-based compilation - Mark
FieldConfig.extractionas@deprecatedin types.ts
Phase 3: Execution + Inngest
triggerTask()— execution function + chat creation + teststaskDispatchinngest function — replaces 4 automation functionstaskCroninngest function — replaces automationCronScan- Update heartbeat
buildAttentionSnapshot()to usegetClaimableTasks()
Phase 4: UI + block + tool
tasksblock definition + resolver + tests- Task card component (with subtasks, run status, trigger button, assignee)
- Task editor component (create/edit sheet with agent/human assignment)
- Task run history component (with chat link)
- Entity type admin: "Tasks" tab
- Tenant-wide
/taskspage (backlog, my tasks, by agent) manageTasksagent tool + tests- Register block + tool in bootstrap
Phase 5: Deprecation + documentation
- Mark
statusTriggers,FieldConfig.actionsas@deprecated - Update entity detail default views to include tasks block
- Feature documentation:
content/docs/features/tasks.mdx - Changelog entry
7. Acceptance criteria
-
taskstable created with RLS, indexes, parent_id self-reference, no scope_check -
workflow_node_runshastask_id+chat_idcolumns - 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 viaagent_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:
manageTaskssupports create, list, trigger, assign - Block:
tasksrenders on entity detail with full tree + status + assignees - Admin tab: "Tasks" on entity type admin page
-
/taskspage with backlog, my tasks, by agent views - All new files have co-located tests
-
pnpm typecheck && pnpm test && pnpm buildpass - 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 treebacklog— flat list of unassigned tasks, no groupingagent— tasks assigned to one agent, grouped by entityuser— 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:
- Basics — name, slug (auto-generated), description
- Instructions — textarea for the SOP/prompt (the core content)
- Completion criteria — textarea for how to determine success
- Assignment — agent selector OR user selector OR "Unassigned (claimable)"
- 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
- Output — output_type selector, conditional config:
- field: field name(s) selector, extraction sources
- entity: target entity type slug
- none: no config
- 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 cardlayout: "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 Tasks —
getUserTasks(tenantId, currentUserId)— tasks assigned to me - Backlog —
getTaskBacklog(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)
| File | Lines | What to do |
|---|---|---|
features/inngest/functions/automation-cron.ts | ~91 | Replace 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 | ~27 | Replace with direct triggerTask() call from task-dispatch.ts. This was a thin wrapper. |
features/inngest/functions/automation-entity-trigger.ts | ~232 | Replace 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 | ~533 | Reuse 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 | ~61 | Replace with task-dispatch handling field lifecycle events. Keep working during transition. |
9b. Files to MODIFY (add tasks integration)
| File | Change |
|---|---|
features/workflows/types.ts | Add task_id: string | null to WorkflowNodeRunRecord. Add chat_id: string | null if not already present. |
features/workflows/compile.ts | Add compileTasksToWorkflow(tasks: TaskRecord[], parent: TaskRecord): WorkflowDefinition — converts task tree into workflow nodes. Reuse existing node creation logic. |
features/workflows/run-workflow.ts | Accept 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.ts | Before 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.ts | Add 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.ts | Add TASK_TRIGGERED: "task/triggered" to EVENT_NAMES with typed event data. |
features/blocks/definitions/index.ts | Import and register tasksDefinition from ./tasks.ts. |
features/blocks/lib/block-metadata.ts | Add metadata entry for "tasks" block type (icon: "Zap", label: "Tasks", etc.). |
features/tools/admin/index.ts | Add manageTasks: "entity_types.team.update" to ADMIN_TOOL_PERMISSIONS. Add ...getTaskAdminTools(ctx) to buildAdminToolSet(). |
features/tools/bootstrap.ts | Import task tool definitions in bootstrapToolRegistry(). |
features/entities/types.ts | Add @deprecated JSDoc to: FieldConfig.extraction, FieldConfig.actions, EntityTypeConfig.statusTriggers, StatusTransitionTrigger. Each with "Use tasks instead" migration note. |
features/views/lib/default-blocks.ts | In generateDefaultBlocks(), conditionally add a tasks block when the entity type has active tasks. |
9c. Files to REUSE (call from tasks, don't modify)
| File | What tasks call |
|---|---|
features/workflows/run-workflow.ts | runEntityWorkflow() for system extraction tasks |
features/agents/runtime.ts | executeAgentSync() for custom task execution |
features/agents/agent-resolver.ts | resolveAgent() to resolve agent_slug to full agent |
features/agents/lib/build-context.ts | loadAgentPromptContext(), buildAgentSystemPrompt() |
features/agents/lib/build-tools.ts | buildInternalAgentTools() for task agent's tool set |
features/agents/heartbeat.ts | shouldRunNow() for cron schedule matching |
features/workflows/trigger-automation-workflow.ts | resolvePromptTemplate() for {field} substitution |
features/entities/extraction/dep-sort.ts | sortFieldsByDependency() for depends_on resolution |
features/entities/extraction/consensus.ts | Consensus execution for tasks with consensus config |
features/chat/server/actions.ts | createChat(), 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 slugsfeatures/agents/lib/attention-context.ts— queries for goals/tasks in heartbeatfeatures/agents/lib/goal-context.ts— formats goals for agent prompts
Migration path:
- 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.
- 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.
View/Block System Cleanup (v3)
Unified workspace editor, response-integrated field blocks, container removal, block config schemas, and consistent editing experience across admin and entity detail pages.
Entity Table Migration to Generic DataTable
Migrate entity list pages from the monolithic data table to the generic DataTable<T> platform component