Tasks

Track work items, issues, and agent activity with lifecycle management

Use tasks when work needs a durable lifecycle, not just a one-shot message: a status that changes over time, a priority, an assignee, comments, tags, attachments, and an audit trail. Start with TaskClient; Hono apps can use c.var.task after installing the Agentuity middleware.

npm install @agentuity/task
import { TaskClient } from '@agentuity/task';
import type { UserEntityRef } from '@agentuity/task';
 
const tasks = new TaskClient();
 
const reporter = {
  id: 'agent_triage',
  name: 'Triage Agent',
  type: 'agent',
} satisfies UserEntityRef;
 
export async function fileBug(errorMessage: string, traceId: string): Promise<string> {
  const task = await tasks.create({
    title: 'Payment flow error',
    type: 'bug',
    priority: 'high',
    description: errorMessage,
    created_id: reporter.id,
    creator: reporter,
    metadata: { traceId, source: 'checkout' },
  });
 
  return task.id;
}

TaskClient reads AGENTUITY_SDK_KEY, then AGENTUITY_CLI_KEY, from the environment. Keep that key in .env for local development and configure the same variable for deployed apps.

When to use tasks

NeedUse
work items with status, priority, comments, and audit historyTasks
fire-and-forget async handoffQueues
recurring timed deliverySchedules
inbound HTTP events with receiptsWebhooks
exact key lookup or countersKey-Value Storage

Client Setup

Construct the client once at module scope and reuse it from handlers, routes, or scripts.

import { TaskClient } from '@agentuity/task';
 
const tasks = new TaskClient({
  orgId: process.env.AGENTUITY_CLOUD_ORG_ID,
});
OptionDescription
apiKeyOptional API key. Defaults to AGENTUITY_SDK_KEY, then AGENTUITY_CLI_KEY.
orgIdOptional organization ID. Used when the API key is org-scoped or when calling from a CLI context.
urlOptional Task API URL. Defaults to AGENTUITY_TASK_URL, then the regional Agentuity service URL.
loggerOptional logger instance.

Create Tasks

create() requires title, type, and created_id. Pass creator so dashboards and history can show a display name and whether the actor is a human or an agent.

const reviewer = {
  id: 'user_123',
  name: 'Maya Chen',
  type: 'human',
} satisfies UserEntityRef;
 
const task = await tasks.create({
  title: 'Review suspicious refund',
  type: 'task',
  priority: 'medium',
  created_id: reporter.id,
  creator: reporter,
  assignee: reviewer,
  metadata: {
    refundId: 'rf_123',
    riskScore: 0.91,
  },
});
FieldRequiredDescription
titleYesUp to 1024 characters.
typeYesOne of epic, feature, enhancement, bug, task.
created_idYesID of the creating user, service, or agent.
creatorNo{ id, name, type? } reference for readable attribution.
descriptionNoUp to 65,536 characters.
priorityNohigh, medium, low, or none. Defaults to none.
statusNoInitial status. Defaults to open.
assigneeNo{ id, name, type? } reference for the assigned actor.
parent_idNoParent task ID for epics, features, or subtasks.
tag_idsNoExisting tag IDs to attach at creation.
metadataNoJSON metadata for trace IDs, integration IDs, or custom fields.
projectNo{ id, name } project reference.

Update Lifecycle

Tasks use four canonical statuses. The client also accepts started, completed, and closed, and normalizes them server-side.

StatusMeaning
openCreated, not started.
in_progressActively being worked on.
doneWork completed.
cancelledWork abandoned.
const claimed = await tasks.update(task.id, {
  status: 'in_progress',
  assignee: reporter,
});
 
const closed = await tasks.close(claimed.id);

Use close(id) for completed work. Use update(id, { status: 'cancelled' }) when the task should be abandoned.

Comments and Tags

Comments take the task ID, body, and author user ID. Pass author so the comment lists carry a display name and type.

await tasks.createComment(
  task.id,
  'Refund matched the manual review policy.',
  reporter.id,
  reporter
);
 
const { comments } = await tasks.listComments(task.id, { limit: 20, offset: 0 });
 
const tag = await tasks.createTag('payments', '#00FFFF');
await tasks.addTagToTask(task.id, tag.id);
const tagsForTask = await tasks.listTagsForTask(task.id);

listTags() returns the bare array. listTagsForTask() returns the tags currently attached to one task.

Attachments

Attachments use a two-step upload. Ask the task service for a presigned upload URL, PUT the bytes to that URL, then confirm the attachment so it shows up in listAttachments().

const { attachment, presigned_url } = await tasks.uploadAttachment(task.id, {
  filename: 'refund-log.json',
  content_type: 'application/json',
  size: 1024,
});
 
const upload = await fetch(presigned_url, {
  method: 'PUT',
  body: JSON.stringify({ refundId: 'rf_123' }),
  headers: { 'Content-Type': 'application/json' },
});
if (!upload.ok) {
  throw new Error(`Attachment upload failed (${upload.status})`);
}
 
const confirmed = await tasks.confirmAttachment(attachment.id);
const { presigned_url: downloadUrl, expiry_seconds } = await tasks.downloadAttachment(confirmed.id);

Presigned URLs expire after expiry_seconds. Request a fresh URL when a user needs to upload or read the file again.

File a Task from a Server Route

Filing a task is a typical pattern when an agent or background worker spots something that needs human follow-up. The route validates the payload, calls tasks.create(), and returns the new task ID.

typescriptsrc/routes/tasks.ts
import { Hono } from 'hono';
import { agentuity } from '@agentuity/hono';
import type { Services } from '@agentuity/hono';
 
type Variables = Pick<Services, 'task'>;
 
const app = new Hono<{ Variables: Variables }>();
 
app.use('*', agentuity());
 
app.post('/tasks/refund-review', async (c) => {
  const body = await c.req.json<{
    refundId: string;
    note: string;
    reporter: { id: string; name: string };
  }>();
 
  const task = await c.var.task.create({
    title: `Review refund ${body.refundId}`,
    type: 'task',
    priority: 'medium',
    description: body.note,
    created_id: body.reporter.id,
    creator: { id: body.reporter.id, name: body.reporter.name, type: 'human' },
    metadata: { refundId: body.refundId },
  });
 
  return c.json({ taskId: task.id }, 201);
});
 
app.get('/tasks/:id', async (c) => {
  const task = await c.var.task.get(c.req.param('id'));
  return task ? c.json({ task }) : c.json({ error: 'not found' }, 404);
});
 
export default app;

List and Inspect

Use list() for filtered views and changelog() when you need the field-level history for one task.

const { tasks: openBugs, total } = await tasks.list({
  type: 'bug',
  status: 'open',
  priority: 'high',
  include: ['metadata', 'tags'],
  sort: '-created_at',
  limit: 25,
  offset: 0,
});
 
const { changelog } = await tasks.changelog(task.id, { limit: 50, offset: 0 });
FilterDescription
status, type, priorityFilter by canonical lifecycle, classification, or priority.
assigned_idFilter by assigned actor ID.
created_idFilter by creator ID.
parent_idReturn subtasks for a parent task.
project_idFilter by project ID.
tag_idFilter by tag ID.
includeAdd fields to the summary response: description, metadata, tags, subtask_count, created_id, deleted.
sortSort by created_at, updated_at, or priority. Prefix with - for descending.
orderOptional sort direction, asc or desc.
limit / offsetPagination controls.
deletedInclude soft-deleted tasks.

list() returns a reduced summary by default. Use include when a dashboard needs description, tags, or subtask counts in the same call.

Soft Delete Tasks

softDelete() removes a task from normal work views without erasing its history.

const deleted = await tasks.softDelete(task.id);
 
const { tasks: deletedTasks } = await tasks.list({
  deleted: true,
  include: ['deleted'],
});

Use close() or update(id, { status: 'cancelled' }) for lifecycle changes. Use softDelete() for cleanup or admin flows where the task should no longer appear in default lists.

Activity Counts

getActivity() returns daily counts grouped by canonical status.

const activity = await tasks.getActivity({ days: 30 });

days is optional and accepts values between 7 and 365. The service applies its own default when days is omitted, so pass an explicit value if dashboards depend on a specific range.

Hono Middleware Variant

@agentuity/hono builds TaskClient once and exposes it on c.var.task.

npm install @agentuity/hono hono
import { Hono } from 'hono';
import { agentuity } from '@agentuity/hono';
import type { Services } from '@agentuity/hono';
 
type Variables = Pick<Services, 'task'>;
 
const app = new Hono<{ Variables: Variables }>();
 
app.use('*', agentuity());
 
app.post('/tasks', async (c) => {
  const body = await c.req.json<{ title: string; userId: string; userName: string }>();
 
  const task = await c.var.task.create({
    title: body.title,
    type: 'task',
    created_id: body.userId,
    creator: { id: body.userId, name: body.userName, type: 'human' },
  });
 
  return c.json({ taskId: task.id }, 201);
});
 
app.get('/tasks/:id', async (c) => {
  const task = await c.var.task.get(c.req.param('id'));
  return task ? c.json({ task }) : c.json({ error: 'Task not found' }, 404);
});
 
export default app;

Next Steps