Human-in-the-Loop Agent Task Management for Claude Code
Docs / Tasks
Guide

Task Lifecycle

Everything about how tasks move from Claude to you and back. Status flows, replies, attachments, and how the agent picks up where it left off.

The Full Lifecycle

notstarted
task created
agent picks up
ongoing
agent working
completed
rejected

The agent sets notstarted → ongoing when it starts work, then transitions to completed or rejected based on your response. For human-assigned tasks, you control the status transitions from the dashboard.

Status Reference

notstarted
Default state when a task is created via createTask. The task is queued and visible in the dashboard. The agent should immediately call updateTaskStatus("ongoing") when it starts working on it.
ongoing
The agent is actively working on this task. The dashboard shows a spinner. The human can still reply at any time, and the agent will receive the message via the notification channel.
completed
The task is done. Set by calling updateTaskStatus("completed"). For human-assigned tasks, the human marks it complete from the dashboard. The task moves to the completed section.
rejected
The task was denied or cancelled. The human can reject from the dashboard, or the agent can call updateTaskStatus("rejected") if it can't complete the work. A reply explaining why is good practice.

Creating Tasks (from Claude)

Claude calls createTask whenever it needs human input. A well-formed task includes:

Claude creates a task
createTask({
  title: "Approve: Delete stale user sessions older than 90 days",
  body: `I'm about to run this migration:
DELETE FROM user_sessions WHERE created_at < NOW() - INTERVAL '90 days';

Estimated rows affected: ~14,000
This is irreversible. Approve to proceed?`,
  assignee: "human"
})
title
Short, action-oriented. Prefixing with "Approve:", "Review:", "Question:" helps you triage at a glance.
body
Full context — what Claude has done, what it needs, relevant code or diffs. Don't make the human dig.
assignee
"human" for tasks requiring your attention. "agent" for tasks Claude assigns to itself (self-tracking).

Replying to Tasks

From the dashboard, type your reply and hit send. Your message is instantly pushed to Claude via the notification channel. Claude receives it as a <channel> message in its context — no polling needed.

How it flows
H
Approved! But use UUID primary key instead of auto-increment. Also add index on expires_at.
Claude received your reply via notification channel.
Updating schema to use UUID pk + expires_at index...
C

The agent can also send replies back to a task thread using the reply MCP tool — useful for progress updates, confirmations, or follow-up questions.

Attachments

Both Claude and humans can attach files to tasks. This is useful for sharing diffs, screenshots, logs, or config files.

Claude Attaches

Claude passes base64-encoded file data to createTask or reply via the attachments parameter.

reply({
  chat_id: "...",
  text: "Here's the diff",
  attachments: [{
    id: "att_001",
    filename: "schema.diff",
    mimeType: "text/plain",
    data: "<base64>"
  }]
})
Claude Downloads

When you attach a file in the dashboard, Claude receives the attachment ID in the channel message. It calls downloadAttachment to fetch it.

downloadAttachment({
  attachment_id: "att_xyz"
})
// returns: { data, filename, mimeType }

Reading Task History

Claude can fetch the full message history of any task using getTaskMessages. This is useful when resuming a long-running task or re-reading instructions after a context reset.

Example
getTaskMessages({
  task_id: "0ZRgCquBZ7R",
  limit: 20,
  cursor: null  // paginate with cursor from previous call
})

// Returns:
{
  messages: [
    { role: "agent", text: "I'm about to delete 14k rows...", ts: "..." },
    { role: "human", text: "Approved, but use UUID...", ts: "..." }
  ],
  nextCursor: null  // null = no more pages
}
Want the full tool reference?
All 6 MCP tools with parameters, types, and examples.