Tasks — Agentuity Documentation

Tasks

Track work items, issues, and agent activity with built-in lifecycle management

Use ctx.task to create and manage structured work items: bugs, features, epics, and general tasks, across agents and human collaborators, with built-in status tracking, comments, tags, and file attachments.

When to Use Tasks

ServiceBest For
TasksStructured work items with lifecycle, assignments, comments, tags
QueuesAsync message passing for background processing
Key-ValueSimple state, caching, counters

Use tasks when you need to:

  • Track bugs or issues an agent discovers during execution
  • Assign work items to humans or agents with explicit status transitions
  • Attach files, comments, and audit trails to work items
  • Build hierarchical project structures (epics, features, subtasks)
  • Query task history by status, priority, type, or assignee

Managing Tasks

MethodBest For
SDK (ctx.task)Agents and routes creating or updating tasks programmatically
CLI (agentuity cloud task)Human and agent CLI workflows, scripting
Web AppVisual task board, manual triage

Creating Tasks

ctx.task.create() requires title, type, and created_id. Include creator (a UserEntityRef) as well to preserve display name and attribution type. All other fields are optional.

import { createAgent } from '@agentuity/runtime';
 
const agent = createAgent('BugReporter', {
  handler: async (ctx, input) => {
    const task = await ctx.task.create({ 
      title: 'Null pointer in payment flow',
      type: 'bug', 
      priority: 'high',
      description: `Observed at ${new Date().toISOString()}: ${input.errorMessage}`,
      created_id: ctx.current.id,
      creator: {
        id: ctx.current.id,
        name: ctx.current.name,
        type: 'agent', // distinguish from human users
      },
      metadata: {
        source: 'payment-service',
        traceId: input.traceId,
      },
    });
 
    ctx.logger.info('Bug filed', { taskId: task.id, status: task.status });
    return { taskId: task.id };
  },
});

CreateTaskParams fields:

FieldRequiredDescription
titleYesTask title, max 1,024 characters
typeYesTask classification ('epic', 'feature', 'enhancement', 'bug', 'task')
created_idYesID of the creating user or agent
creatorNoUserEntityRef with id, name, and optional type; adds display name alongside created_id
descriptionNoDetailed description, max 65,536 characters
priorityNo'high', 'medium', 'low', or 'none' (default: 'none')
statusNoInitial status (default: 'open')
assigneeNoUserEntityRef to assign the task to
parent_idNoID of a parent task for hierarchical organization
tag_idsNoArray of tag IDs to attach at creation
metadataNoArbitrary key-value metadata for custom fields
projectNoEntityRef linking the task to a project

Task Lifecycle

Tasks move through a defined set of statuses. The server automatically records timestamps when each transition occurs.

StatusDescriptionDate field set
openCreated, not yet startedopen_date
in_progressActively being worked onin_progress_date
doneWork completedclosed_date
cancelledAbandonedcancelled_date
import { createAgent } from '@agentuity/runtime';
 
const agent = createAgent('TaskWorker', {
  handler: async (ctx, input) => {
    const task = await ctx.task.get(input.taskId);
    if (!task) return { error: 'Task not found' };
 
    // Claim the task before starting work
    await ctx.task.update(task.id, { 
      status: 'in_progress',
      assignee: { id: ctx.current.id, name: ctx.current.name, type: 'agent' },
    });
 
    // ... do the work ...
 
    // Mark complete when done
    await ctx.task.update(task.id, {
      status: 'done', 
    });
 
    return { completed: task.id };
  },
});

Task Types and Priority

Types classify what a task represents:

TypeWhen to use
epicLarge initiatives spanning multiple features or tasks
featureNew capabilities to build
enhancementImprovements to existing functionality
bugDefects to fix
taskGeneral work items

Priority signals urgency:

PriorityDescription
highRequires immediate attention
mediumStandard priority
lowBackground or nice-to-have work
noneNo priority assigned (default)

Human vs Agent Attribution

UserEntityRef carries a type field that distinguishes human users from AI agents. This lets dashboards and queries filter by who (or what) created or is working on tasks.

import { createAgent } from '@agentuity/runtime';
 
const agent = createAgent('TaskRouter', {
  handler: async (ctx, input) => {
    // Created by this agent, assigned to a human for review
    const task = await ctx.task.create({
      title: 'Review anomaly in transaction log',
      type: 'task',
      created_id: ctx.current.id,
      creator: {
        id: ctx.current.id,
        name: ctx.current.name,
        type: 'agent', 
      },
      assignee: {
        id: input.reviewerId,
        name: input.reviewerName,
        type: 'human', 
      },
      priority: 'medium',
    });
 
    ctx.logger.info('Task routed to human', {
      taskId: task.id,
      assignee: task.assignee?.name,
    });
 
    return { taskId: task.id };
  },
});

Comments

Add threaded comments to any task. userId identifies the commenter; author is an optional UserEntityRef ({ id, name, type? }) that adds a display name and optional human/agent type discrimination.

import { createAgent } from '@agentuity/runtime';
 
const agent = createAgent('TaskCommentWriter', {
  handler: async (ctx, input) => {
    // Add a comment
    const comment = await ctx.task.createComment( 
      input.taskId,
      `Analysis complete. Root cause: ${input.finding}`,
      ctx.current.id,
      { id: ctx.current.id, name: ctx.current.name },
    );
 
    // Retrieve all comments with pagination
    const { comments, total } = await ctx.task.listComments(input.taskId, {
      limit: 20,
      offset: 0,
    });
 
    ctx.logger.info('Comments loaded', { total });
    return { commentId: comment.id, total };
  },
});

Tags

Tags are org-wide labels you create once and apply to multiple tasks. Create a tag, then associate it.

import { createAgent } from '@agentuity/runtime';
 
const agent = createAgent('TagManager', {
  handler: async (ctx, input) => {
    // Create a reusable tag (once per org)
    const tag = await ctx.task.createTag('payment-system', '#ff6600'); 
 
    // Apply it to a task
    await ctx.task.addTagToTask(input.taskId, tag.id); 
 
    // List all tags on a task
    const tags = await ctx.task.listTagsForTask(input.taskId);
 
    ctx.logger.info('Tags applied', { tagCount: tags.length });
    return { tags };
  },
});

Attachments

Attachments use a two-phase upload: first call uploadAttachment() to get a presigned S3 URL, PUT your file bytes directly to that URL, then call confirmAttachment() to mark the upload complete.

import { createAgent } from '@agentuity/runtime';
 
const agent = createAgent('AttachmentUploader', {
  handler: async (ctx, input) => {
    // Phase 1: request a presigned upload URL
    const { attachment, presigned_url } = await ctx.task.uploadAttachment( 
      input.taskId,
      {
        filename: 'error-trace.pdf',
        content_type: 'application/pdf',
        size: input.fileBytes.length,
      },
    );
 
    // Phase 2: upload directly to S3 (bypasses Agentuity servers)
    const uploadResponse = await fetch(presigned_url, { 
      method: 'PUT',
      body: input.fileBytes,
      headers: { 'Content-Type': 'application/pdf' },
    });
 
    if (!uploadResponse.ok) {
      ctx.logger.error('Upload failed', { status: uploadResponse.status });
      return { error: 'Upload failed' };
    }
 
    // Phase 3: confirm the upload so the attachment becomes visible
    const confirmed = await ctx.task.confirmAttachment(attachment.id); 
    ctx.logger.info('Attachment confirmed', { attachmentId: confirmed.id });
 
    // Download: get a presigned URL to read the file back
    const { presigned_url: downloadUrl } = await ctx.task.downloadAttachment(attachment.id);
 
    return { attachmentId: confirmed.id, downloadUrl };
  },
});

Filtering and Pagination

ctx.task.list() accepts filters to narrow results. All parameters are optional.

import { createAgent } from '@agentuity/runtime';
 
const agent = createAgent('TaskDashboard', {
  handler: async (ctx, input) => {
    // High-priority open bugs, newest first
    const { tasks, total } = await ctx.task.list({ 
      type: 'bug',
      status: 'open',
      priority: 'high',
      sort: '-created_at', // descending by creation date
      limit: 25,
      offset: 0,
    });
 
    // Subtasks of a specific epic
    const { tasks: subtasks } = await ctx.task.list({
      parent_id: input.epicId, 
      limit: 50,
    });
 
    ctx.logger.info('Tasks loaded', { total, subtaskCount: subtasks.length });
    return { tasks, subtasks };
  },
});

ListTasksParams options:

FieldDescription
statusFilter by lifecycle status
typeFilter by task type
priorityFilter by priority level
assigned_idFilter by assigned user ID
parent_idFilter by parent task (returns subtasks)
project_idFilter by project ID
tag_idFilter by a specific tag
sortSort field: 'created_at', 'updated_at', 'priority'. Prefix with - for descending
orderSort direction: 'asc' or 'desc'
limitMaximum results to return
offsetResults to skip for pagination
deletedInclude soft-deleted tasks (default: false)

Changelog

Every field change on a task is automatically recorded. Use ctx.task.changelog() to retrieve the full audit trail.

import { createAgent } from '@agentuity/runtime';
 
const agent = createAgent('TaskAuditor', {
  handler: async (ctx, input) => {
    const { changelog, total } = await ctx.task.changelog(input.taskId, { 
      limit: 50,
      offset: 0,
    });
 
    for (const entry of changelog) {
      ctx.logger.info('Field changed', {
        field: entry.field,
        from: entry.old_value,
        to: entry.new_value,
        at: entry.created_at,
      });
    }
 
    return { changes: total };
  },
});

Each TaskChangelogEntry records the field name, old_value, new_value, and created_at timestamp. Status transitions, priority changes, and reassignments all appear automatically, with no extra instrumentation needed.

Batch Operations

batchDelete soft-deletes tasks matching a set of filters. Soft-deleted tasks are hidden from normal queries unless you pass deleted: true to list().

import { createAgent } from '@agentuity/runtime';
 
const agent = createAgent('TaskCleaner', {
  handler: async (ctx, input) => {
    // Remove cancelled tasks older than 30 days (up to 200 at a time)
    const { deleted, count } = await ctx.task.batchDelete({ 
      status: 'cancelled',
      older_than: '30d', // Go-style duration: '30m', '24h', '7d', '2w'
      limit: 200,
    });
 
    ctx.logger.info('Batch delete complete', { count });
    return { deletedCount: count };
  },
});

Hierarchical Tasks

Set parent_id to build epic-to-feature-to-task hierarchies. Query subtasks with list({ parent_id: epicId }). List responses include a subtask_count field with the number of direct child tasks, so you can display task counts without fetching all subtasks.

import { createAgent } from '@agentuity/runtime';
 
const agent = createAgent('ProjectPlanner', {
  handler: async (ctx, input) => {
    // Create the top-level epic
    const epic = await ctx.task.create({
      title: 'Auth system overhaul',
      type: 'epic',
      priority: 'high',
      created_id: ctx.current.id,
    });
 
    // Create features under the epic
    const feature = await ctx.task.create({
      title: 'Implement OAuth2 login',
      type: 'feature',
      parent_id: epic.id, 
      created_id: ctx.current.id,
    });
 
    // Create a task under the feature
    await ctx.task.create({
      title: 'Write OAuth2 callback handler',
      type: 'task',
      parent_id: feature.id, 
      created_id: ctx.current.id,
    });
 
    ctx.logger.info('Project hierarchy created', { epicId: epic.id });
    return { epicId: epic.id };
  },
});

Using in Routes

Routes access the same task service via c.var.task:

import { createRouter } from '@agentuity/runtime';
 
const router = createRouter();
 
router.post('/tasks', async (c) => {
  const body = await c.req.json();
 
  const task = await c.var.task.create({ 
    title: body.title,
    type: body.type ?? 'task',
    created_id: body.userId,
    creator: {
      id: body.userId,
      name: body.userName,
      type: 'human',
    },
  });
 
  return c.json({ taskId: task.id }, 201);
});
 
router.get('/tasks/:id', async (c) => {
  const task = await c.var.task.get(c.req.param('id')); 
 
  if (!task) {
    return c.json({ error: 'Not found' }, 404);
  }
 
  return c.json(task);
});
 
export default router;

Best Practices

  • Use creator instead of created_id: The creator field stores display name and type alongside the ID, which makes task history readable without joining user records.
  • Set type: 'agent' for programmatic tasks: This lets dashboards distinguish automated work items from human-filed tickets.
  • Prefer update() over ad-hoc field sets: Partial updates only modify provided fields, so you won't accidentally overwrite data you didn't intend to change.
  • Use metadata for integration fields: Store external IDs, trace IDs, or service names in metadata rather than encoding them in the title or description.
  • Page results with limit and offset: There is no default page cap, so always set an explicit limit when listing tasks in agents to avoid unbounded responses.

Next Steps

  • Queues: Async message passing for background processing between agents
  • Key-Value Storage: Simple per-session state and counters
  • Agents: Creating agents that use ctx.task and other services