Skip to main content
Glama

board_update_task

Update a task's status, assignment, priority, RIPER mode, or project. Move tasks between projects while preserving subtask relationships manually.

Instructions

Update a task's status, assignment, priority, RIPER mode, project, or other fields. Pass project_id to move the task to a different project (the target project must exist; subtasks are NOT auto-moved — caller must move them separately if needed).

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
task_idYesTask ID to update
statusNoNew status
priorityNoNew priority
assigned_agentNoNew agent assignment (empty string to unassign)
riper_modeNoNew RIPER mode
titleNoUpdated title
descriptionNoUpdated description
depends_onNoUpdated dependency list
metadataNoMetadata to merge
project_idNoMove task to this project. Target project must exist. Subtasks are NOT auto-moved — their parent_task_id link will cross projects unless the caller also moves them. Returns a warning in the message if the task has subtasks still in the source project.

Implementation Reference

  • Handler function for board_update_task: fetches the task, validates existence, handles field updates (status, priority, assigned_agent, riper_mode, title, description, depends_on, metadata), supports project reassignment with target validation and subtask orphan warnings, manages started_at/completed_at timestamps based on status transitions, and writes an activity_log entry. Returns { id, changes, warnings?, message }.
      async ({
        task_id,
        status,
        priority,
        assigned_agent,
        riper_mode,
        title,
        description,
        depends_on,
        metadata,
        project_id,
      }) => {
        const taskRef = db.collection("tasks").doc(task_id);
        const taskSnap = await taskRef.get();
    
        if (!taskSnap.exists) {
          return {
            content: [
              {
                type: "text" as const,
                text: JSON.stringify({ error: `Task ${task_id} not found` }),
              },
            ],
          };
        }
    
        const oldData = taskSnap.data()!;
        const now = Timestamp.now();
        const updates: Record<string, unknown> = { updated_at: now };
        const changes: string[] = [];
        const warnings: string[] = [];
    
        // Project reassignment — validate target project exists before writing.
        // Uses a single-field subtask query (parent_task_id only) then JS-side
        // project_id filtering to avoid requiring a Firestore composite index.
        let projectMoveApplied = false;
        if (project_id !== undefined && project_id !== oldData.project_id) {
          const targetProjSnap = await db.collection("projects").doc(project_id).get();
          if (!targetProjSnap.exists) {
            return {
              content: [
                {
                  type: "text" as const,
                  text: JSON.stringify({
                    error: `Target project ${project_id} not found. Task not moved.`,
                  }),
                },
              ],
            };
          }
    
          // Resolve names for the audit log (both projects). Old project name
          // is best-effort: legacy tasks may have no project_id, or the old
          // project may have been deleted since the task was created.
          let oldProjName: string = "(none)";
          if (oldData.project_id) {
            const oldProjSnap = await db
              .collection("projects")
              .doc(oldData.project_id)
              .get();
            oldProjName = oldProjSnap.exists
              ? (oldProjSnap.data()?.name ?? oldData.project_id)
              : `${oldData.project_id} (deleted)`;
          }
          const newProjName = targetProjSnap.data()?.name ?? project_id;
          const oldProjIdDisplay = oldData.project_id ?? "none";
    
          updates.project_id = project_id;
          changes.push(
            `project: ${oldProjName} (${oldProjIdDisplay}) → ${newProjName} (${project_id})`
          );
          projectMoveApplied = true;
    
          // Detect subtasks that'd be orphaned in the source project after the
          // move. Query by parent_task_id only (single field, no index needed),
          // then filter by source project_id in JS. Skip entirely when the task
          // is a legacy row with no source project_id — undefined in a Firestore
          // .where clause would throw.
          const subtaskSnap = await db
            .collection("tasks")
            .where("parent_task_id", "==", task_id)
            .get();
          if (oldData.project_id && !subtaskSnap.empty) {
            const stillInSource = subtaskSnap.docs.filter(
              (d) => d.data().project_id === oldData.project_id
            );
            if (stillInSource.length > 0) {
              warnings.push(
                `${stillInSource.length} subtask(s) still in source project ${oldData.project_id}. Move them separately if desired.`
              );
            }
          }
        }
    
        if (status !== undefined) {
          updates.status = status;
          changes.push(`status: ${oldData.status} → ${status}`);
    
          // Set started_at when first moving to in_progress (preserve on re-entry from blocked/review)
          if (status === "in_progress" && !oldData.started_at) {
            updates.started_at = now;
          }
          // Clear started_at if task is sent back to todo/backlog (genuinely un-started)
          if ((status === "todo" || status === "backlog") && oldData.started_at) {
            updates.started_at = null;
          }
    
          if (status === "done" && oldData.status !== "done") {
            updates.completed_at = now;
          } else if (status !== "done" && oldData.status === "done") {
            updates.completed_at = null;
          }
        }
    
        if (priority !== undefined) {
          updates.priority = priority;
          changes.push(`priority: ${oldData.priority} → ${priority}`);
        }
    
        if (assigned_agent !== undefined) {
          updates.assigned_agent = assigned_agent === "" ? null : assigned_agent;
          changes.push(
            `assigned: ${oldData.assigned_agent ?? "none"} → ${assigned_agent || "none"}`
          );
        }
    
        if (riper_mode !== undefined) {
          updates.riper_mode = riper_mode;
          changes.push(`riper_mode: ${oldData.riper_mode ?? "none"} → ${riper_mode}`);
        }
    
        if (title !== undefined) {
          updates.title = title;
          changes.push(`title updated`);
        }
    
        if (description !== undefined) {
          updates.description = description;
          changes.push(`description updated`);
        }
    
        if (depends_on !== undefined) {
          updates.depends_on = depends_on;
          changes.push(`dependencies updated`);
        }
    
        if (metadata !== undefined) {
          updates.metadata = { ...oldData.metadata, ...metadata };
          changes.push(`metadata updated`);
        }
    
        await taskRef.update(updates);
    
        const action = status === "done" ? "completed" :
                       status === "blocked" ? "blocked" :
                       assigned_agent !== undefined ? "claimed" :
                       riper_mode !== undefined ? "mode_changed" : "updated";
    
        const logMetadata: Record<string, unknown> = {};
        if (updates.project_id !== undefined) {
          logMetadata.project_id_from = oldData.project_id ?? null;
          logMetadata.project_id_to = updates.project_id;
        }
    
        await db.collection("activity_log").add({
          task_id,
          session_id: null,
          agent_name: (assigned_agent !== undefined && assigned_agent !== "")
            ? assigned_agent
            : oldData.assigned_agent ?? "system",
          action,
          details: changes.join(", "),
          metadata: logMetadata,
          created_at: now,
        });
    
        const responseMessage =
          warnings.length > 0
            ? `Task updated: ${changes.join(", ")}. Warnings: ${warnings.join("; ")}`
            : `Task updated: ${changes.join(", ")}`;
    
        return {
          content: [
            {
              type: "text" as const,
              text: JSON.stringify(
                {
                  id: task_id,
                  changes,
                  warnings: warnings.length > 0 ? warnings : undefined,
                  message: responseMessage,
                },
                null,
                2
              ),
            },
          ],
        };
      }
    );
  • Input schema (Zod) for board_update_task — defines all updatable fields with their types and descriptions.
    {
      task_id: z.string().describe("Task ID to update"),
      status: z
        .enum(["backlog", "todo", "in_progress", "blocked", "review", "done"])
        .optional()
        .describe("New status"),
      priority: z
        .enum(["critical", "high", "medium", "low"])
        .optional()
        .describe("New priority"),
      assigned_agent: z
        .string()
        .optional()
        .describe("New agent assignment (empty string to unassign)"),
      riper_mode: z
        .enum(["research", "innovate", "plan", "execute", "review", "commit"])
        .optional()
        .describe("New RIPER mode"),
      title: z.string().optional().describe("Updated title"),
      description: z.string().optional().describe("Updated description"),
      depends_on: z
        .array(z.string())
        .optional()
        .describe("Updated dependency list"),
      metadata: z
        .record(z.string(), z.unknown())
        .optional()
        .describe("Metadata to merge"),
      project_id: z
        .string()
        .optional()
        .describe(
          "Move task to this project. Target project must exist. Subtasks are NOT auto-moved — their parent_task_id link will cross projects unless the caller also moves them. Returns a warning in the message if the task has subtasks still in the source project."
        ),
    },
  • Registration of board_update_task via server.tool() inside registerTaskTools(), exported from src/tools/tasks.ts and called from src/index.ts.
    server.tool(
      "board_update_task",
      "Update a task's status, assignment, priority, RIPER mode, project, or other fields. Pass project_id to move the task to a different project (the target project must exist; subtasks are NOT auto-moved — caller must move them separately if needed).",
      {
        task_id: z.string().describe("Task ID to update"),
        status: z
          .enum(["backlog", "todo", "in_progress", "blocked", "review", "done"])
          .optional()
          .describe("New status"),
        priority: z
          .enum(["critical", "high", "medium", "low"])
          .optional()
          .describe("New priority"),
        assigned_agent: z
          .string()
          .optional()
          .describe("New agent assignment (empty string to unassign)"),
        riper_mode: z
          .enum(["research", "innovate", "plan", "execute", "review", "commit"])
          .optional()
          .describe("New RIPER mode"),
        title: z.string().optional().describe("Updated title"),
        description: z.string().optional().describe("Updated description"),
        depends_on: z
          .array(z.string())
          .optional()
          .describe("Updated dependency list"),
        metadata: z
          .record(z.string(), z.unknown())
          .optional()
          .describe("Metadata to merge"),
        project_id: z
          .string()
          .optional()
          .describe(
            "Move task to this project. Target project must exist. Subtasks are NOT auto-moved — their parent_task_id link will cross projects unless the caller also moves them. Returns a warning in the message if the task has subtasks still in the source project."
          ),
      },
      async ({
        task_id,
        status,
        priority,
        assigned_agent,
        riper_mode,
        title,
        description,
        depends_on,
        metadata,
        project_id,
      }) => {
        const taskRef = db.collection("tasks").doc(task_id);
        const taskSnap = await taskRef.get();
    
        if (!taskSnap.exists) {
          return {
            content: [
              {
                type: "text" as const,
                text: JSON.stringify({ error: `Task ${task_id} not found` }),
              },
            ],
          };
        }
    
        const oldData = taskSnap.data()!;
        const now = Timestamp.now();
        const updates: Record<string, unknown> = { updated_at: now };
        const changes: string[] = [];
        const warnings: string[] = [];
    
        // Project reassignment — validate target project exists before writing.
        // Uses a single-field subtask query (parent_task_id only) then JS-side
        // project_id filtering to avoid requiring a Firestore composite index.
        let projectMoveApplied = false;
        if (project_id !== undefined && project_id !== oldData.project_id) {
          const targetProjSnap = await db.collection("projects").doc(project_id).get();
          if (!targetProjSnap.exists) {
            return {
              content: [
                {
                  type: "text" as const,
                  text: JSON.stringify({
                    error: `Target project ${project_id} not found. Task not moved.`,
                  }),
                },
              ],
            };
          }
    
          // Resolve names for the audit log (both projects). Old project name
          // is best-effort: legacy tasks may have no project_id, or the old
          // project may have been deleted since the task was created.
          let oldProjName: string = "(none)";
          if (oldData.project_id) {
            const oldProjSnap = await db
              .collection("projects")
              .doc(oldData.project_id)
              .get();
            oldProjName = oldProjSnap.exists
              ? (oldProjSnap.data()?.name ?? oldData.project_id)
              : `${oldData.project_id} (deleted)`;
          }
          const newProjName = targetProjSnap.data()?.name ?? project_id;
          const oldProjIdDisplay = oldData.project_id ?? "none";
    
          updates.project_id = project_id;
          changes.push(
            `project: ${oldProjName} (${oldProjIdDisplay}) → ${newProjName} (${project_id})`
          );
          projectMoveApplied = true;
    
          // Detect subtasks that'd be orphaned in the source project after the
          // move. Query by parent_task_id only (single field, no index needed),
          // then filter by source project_id in JS. Skip entirely when the task
          // is a legacy row with no source project_id — undefined in a Firestore
          // .where clause would throw.
          const subtaskSnap = await db
            .collection("tasks")
            .where("parent_task_id", "==", task_id)
            .get();
          if (oldData.project_id && !subtaskSnap.empty) {
            const stillInSource = subtaskSnap.docs.filter(
              (d) => d.data().project_id === oldData.project_id
            );
            if (stillInSource.length > 0) {
              warnings.push(
                `${stillInSource.length} subtask(s) still in source project ${oldData.project_id}. Move them separately if desired.`
              );
            }
          }
        }
    
        if (status !== undefined) {
          updates.status = status;
          changes.push(`status: ${oldData.status} → ${status}`);
    
          // Set started_at when first moving to in_progress (preserve on re-entry from blocked/review)
          if (status === "in_progress" && !oldData.started_at) {
            updates.started_at = now;
          }
          // Clear started_at if task is sent back to todo/backlog (genuinely un-started)
          if ((status === "todo" || status === "backlog") && oldData.started_at) {
            updates.started_at = null;
          }
    
          if (status === "done" && oldData.status !== "done") {
            updates.completed_at = now;
          } else if (status !== "done" && oldData.status === "done") {
            updates.completed_at = null;
          }
        }
    
        if (priority !== undefined) {
          updates.priority = priority;
          changes.push(`priority: ${oldData.priority} → ${priority}`);
        }
    
        if (assigned_agent !== undefined) {
          updates.assigned_agent = assigned_agent === "" ? null : assigned_agent;
          changes.push(
            `assigned: ${oldData.assigned_agent ?? "none"} → ${assigned_agent || "none"}`
          );
        }
    
        if (riper_mode !== undefined) {
          updates.riper_mode = riper_mode;
          changes.push(`riper_mode: ${oldData.riper_mode ?? "none"} → ${riper_mode}`);
        }
    
        if (title !== undefined) {
          updates.title = title;
          changes.push(`title updated`);
        }
    
        if (description !== undefined) {
          updates.description = description;
          changes.push(`description updated`);
        }
    
        if (depends_on !== undefined) {
          updates.depends_on = depends_on;
          changes.push(`dependencies updated`);
        }
    
        if (metadata !== undefined) {
          updates.metadata = { ...oldData.metadata, ...metadata };
          changes.push(`metadata updated`);
        }
    
        await taskRef.update(updates);
    
        const action = status === "done" ? "completed" :
                       status === "blocked" ? "blocked" :
                       assigned_agent !== undefined ? "claimed" :
                       riper_mode !== undefined ? "mode_changed" : "updated";
    
        const logMetadata: Record<string, unknown> = {};
        if (updates.project_id !== undefined) {
          logMetadata.project_id_from = oldData.project_id ?? null;
          logMetadata.project_id_to = updates.project_id;
        }
    
        await db.collection("activity_log").add({
          task_id,
          session_id: null,
          agent_name: (assigned_agent !== undefined && assigned_agent !== "")
            ? assigned_agent
            : oldData.assigned_agent ?? "system",
          action,
          details: changes.join(", "),
          metadata: logMetadata,
          created_at: now,
        });
    
        const responseMessage =
          warnings.length > 0
            ? `Task updated: ${changes.join(", ")}. Warnings: ${warnings.join("; ")}`
            : `Task updated: ${changes.join(", ")}`;
    
        return {
          content: [
            {
              type: "text" as const,
              text: JSON.stringify(
                {
                  id: task_id,
                  changes,
                  warnings: warnings.length > 0 ? warnings : undefined,
                  message: responseMessage,
                },
                null,
                2
              ),
            },
          ],
        };
      }
    );
  • Imports for board_update_task: McpServer (tool registration), Firestore/Timestamp (database and timestamps), and Zod (schema validation).
    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It discloses that moving a task to a different project requires the target project to exist and that subtasks are not automatically moved, which is a critical behavioral detail. However, it doesn't mention other side effects like idempotency or auth requirements.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise, consisting of two sentences that front-load the main purpose and immediately follow with the key behavioral caveat. Every sentence adds value with no redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity (10 parameters including nested objects) and no output schema, the description covers the primary purpose and a critical behavioral detail. However, it does not explain the return value or error conditions, which would be helpful but are not required since no output schema exists.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 100% description coverage, so the baseline is 3. The description adds value by explaining the project_id parameter's behavior (subtask handling warning). Other parameters are self-explanatory from the schema descriptions, so no significant additional meaning is needed.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description explicitly states it updates a task's status, assignment, priority, RIPER mode, project, or other fields, distinguishing it from sibling tools like board_create_task or board_delete_task. It uses specific verbs and resources.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description clearly indicates when to use the tool (to update various task fields) and includes a specific warning about moving tasks between projects and the need to handle subtasks separately. While it does not explicitly list alternatives, it provides sufficient context for usage decisions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/HuntsDesk/ve-vibe-board'

If you have feedback or need assistance with the MCP directory API, please join our Discord server