Skip to main content
Glama

board_update_project

Update a project's status, priority, name, description, or metadata. Pause on-hold projects, archive completed ones, or adjust portfolio priority during weekly reviews.

Instructions

Update a project's status (active/paused/completed/archived), priority, name, description, or metadata. Use this to pause projects that are on hold, archive completed projects so they don't clutter the active list, or re-rank portfolio priority during weekly reviews. Pass null to description/metadata to clear them.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
project_idYesProject ID to update
statusNoNew status. 'paused' means intentionally on hold pending capacity or dependency — not stalled, not archived. Revisit at priority review. Valid transitions: active→paused/completed/archived, paused→active/completed/archived, completed→archived/active, archived→active.
priorityNoPortfolio-level importance — drives weekly focus and default sort order in board_get_projects. DISTINCT from task.priority (which orders execution within a single project). Adjust during weekly portfolio reviews to promote/demote initiatives without touching the underlying tasks.
nameNoUpdated name
descriptionNoUpdated description. Pass null to clear; omit to leave unchanged.
metadataNoMetadata to shallow-merge with existing. Pass null to clear all metadata; omit to leave unchanged.

Implementation Reference

  • Handler function for board_update_project tool. Updates a project's fields (status, priority, name, description, metadata) with validation: checks project existence, validates status transitions against allowed transitions map, conditionally updates each field, supports null to clear description/metadata, shallow-merges metadata with order-independent deep equality check, writes audit trail to activity_log, and bumps updated_at timestamp.
      async ({ project_id, status, priority, name, description, metadata }) => {
        const docRef = db.collection("projects").doc(project_id);
        const snap = await docRef.get();
        if (!snap.exists) {
          return {
            content: [
              {
                type: "text" as const,
                text: JSON.stringify(
                  { error: `Project ${project_id} not found` },
                  null,
                  2
                ),
              },
            ],
          };
        }
    
        const existing = (snap.data() ?? {}) as Partial<ProjectData>;
        const updates: Record<string, unknown> = {};
        const changes: string[] = [];
    
        if (status !== undefined && status !== existing.status) {
          const currentStatus = existing.status ?? "active";
          const allowed = validTransitions[currentStatus] ?? [];
          if (!allowed.includes(status)) {
            return {
              content: [
                {
                  type: "text" as const,
                  text: JSON.stringify(
                    {
                      error: `Invalid status transition: ${currentStatus} → ${status}. Allowed from ${currentStatus}: ${allowed.join(", ") || "(none)"}`,
                    },
                    null,
                    2
                  ),
                },
              ],
            };
          }
          updates.status = status;
          changes.push(`status: ${currentStatus} → ${status}`);
        }
    
        if (priority !== undefined && priority !== existing.priority) {
          const prev = existing.priority ?? "medium";
          updates.priority = priority;
          changes.push(`priority: ${prev} → ${priority}`);
        }
    
        if (name !== undefined && name !== existing.name) {
          updates.name = name;
          changes.push(`name updated`);
        }
    
        // description: null clears, string replaces, undefined leaves unchanged
        if (description !== undefined && description !== existing.description) {
          updates.description = description; // can be null or string
          changes.push(description === null ? `description cleared` : `description updated`);
        }
    
        // metadata: null clears, object shallow-merges, undefined leaves unchanged
        if (metadata !== undefined) {
          if (metadata === null) {
            // Only count as a change if there was actually metadata before
            if (existing.metadata && Object.keys(existing.metadata).length > 0) {
              updates.metadata = {};
              changes.push(`metadata cleared`);
            }
          } else {
            const merged = { ...(existing.metadata ?? {}), ...metadata };
            // Skip the change when nothing actually differs after merge.
            // Uses order-independent deepEqual to avoid false-positives from
            // key reordering (V8 auto-sorts numeric keys, spread order can shift).
            if (!deepEqual(merged, existing.metadata ?? {})) {
              updates.metadata = merged;
              changes.push(`metadata merged`);
            }
          }
        }
    
        if (changes.length === 0) {
          return {
            content: [
              {
                type: "text" as const,
                text: JSON.stringify(
                  {
                    id: project_id,
                    name: existing.name ?? null,
                    status: existing.status ?? null,
                    message: "No changes to apply",
                  },
                  null,
                  2
                ),
              },
            ],
          };
        }
    
        // Bump updated_at only when there are real changes
        const now = Timestamp.now();
        updates.updated_at = now;
    
        await docRef.update(updates);
    
        // Mirror tasks.ts: write an activity_log entry for audit trail
        await db.collection("activity_log").add({
          task_id: null,
          session_id: null,
          agent_name: "system",
          action: "updated",
          details: `Project ${project_id}: ${changes.join(", ")}`,
          metadata: { project_id },
          created_at: now,
        });
    
        return {
          content: [
            {
              type: "text" as const,
              text: JSON.stringify(
                {
                  id: project_id,
                  name: (updates.name as string | undefined) ?? existing.name ?? null,
                  status: (updates.status as string | undefined) ?? existing.status ?? null,
                  changes,
                  message: `Project updated: ${changes.join(", ")}`,
                },
                null,
                2
              ),
            },
          ],
        };
      }
    );
  • Zod input schema for board_update_project. Accepts: project_id (string required), status (enum: active/paused/completed/archived, optional), priority (enum: critical/high/medium/low, optional), name (string, optional), description (string or null, optional), and metadata (record or null, optional for shallow-merge).
    {
      project_id: z.string().describe("Project ID to update"),
      status: z
        .enum(["active", "paused", "completed", "archived"])
        .optional()
        .describe("New status. 'paused' means intentionally on hold pending capacity or dependency — not stalled, not archived. Revisit at priority review. Valid transitions: active→paused/completed/archived, paused→active/completed/archived, completed→archived/active, archived→active."),
      priority: z
        .enum(["critical", "high", "medium", "low"])
        .optional()
        .describe("Portfolio-level importance — drives weekly focus and default sort order in board_get_projects. DISTINCT from task.priority (which orders execution within a single project). Adjust during weekly portfolio reviews to promote/demote initiatives without touching the underlying tasks."),
      name: z.string().optional().describe("Updated name"),
      description: z
        .string()
        .nullable()
        .optional()
        .describe("Updated description. Pass null to clear; omit to leave unchanged."),
      metadata: z
        .record(z.string(), z.unknown())
        .nullable()
        .optional()
        .describe("Metadata to shallow-merge with existing. Pass null to clear all metadata; omit to leave unchanged."),
    },
  • Registration of board_update_project tool via server.tool() call within registerProjectTools(), which is exported and invoked in src/index.ts at line 28.
    server.tool(
      "board_update_project",
      "Update a project's status (active/paused/completed/archived), priority, name, description, or metadata. Use this to pause projects that are on hold, archive completed projects so they don't clutter the active list, or re-rank portfolio priority during weekly reviews. Pass null to description/metadata to clear them.",
      {
        project_id: z.string().describe("Project ID to update"),
        status: z
          .enum(["active", "paused", "completed", "archived"])
          .optional()
          .describe("New status. 'paused' means intentionally on hold pending capacity or dependency — not stalled, not archived. Revisit at priority review. Valid transitions: active→paused/completed/archived, paused→active/completed/archived, completed→archived/active, archived→active."),
        priority: z
          .enum(["critical", "high", "medium", "low"])
          .optional()
          .describe("Portfolio-level importance — drives weekly focus and default sort order in board_get_projects. DISTINCT from task.priority (which orders execution within a single project). Adjust during weekly portfolio reviews to promote/demote initiatives without touching the underlying tasks."),
        name: z.string().optional().describe("Updated name"),
        description: z
          .string()
          .nullable()
          .optional()
          .describe("Updated description. Pass null to clear; omit to leave unchanged."),
        metadata: z
          .record(z.string(), z.unknown())
          .nullable()
          .optional()
          .describe("Metadata to shallow-merge with existing. Pass null to clear all metadata; omit to leave unchanged."),
      },
      async ({ project_id, status, priority, name, description, metadata }) => {
        const docRef = db.collection("projects").doc(project_id);
        const snap = await docRef.get();
        if (!snap.exists) {
          return {
            content: [
              {
                type: "text" as const,
                text: JSON.stringify(
                  { error: `Project ${project_id} not found` },
                  null,
                  2
                ),
              },
            ],
          };
        }
    
        const existing = (snap.data() ?? {}) as Partial<ProjectData>;
        const updates: Record<string, unknown> = {};
        const changes: string[] = [];
    
        if (status !== undefined && status !== existing.status) {
          const currentStatus = existing.status ?? "active";
          const allowed = validTransitions[currentStatus] ?? [];
          if (!allowed.includes(status)) {
            return {
              content: [
                {
                  type: "text" as const,
                  text: JSON.stringify(
                    {
                      error: `Invalid status transition: ${currentStatus} → ${status}. Allowed from ${currentStatus}: ${allowed.join(", ") || "(none)"}`,
                    },
                    null,
                    2
                  ),
                },
              ],
            };
          }
          updates.status = status;
          changes.push(`status: ${currentStatus} → ${status}`);
        }
    
        if (priority !== undefined && priority !== existing.priority) {
          const prev = existing.priority ?? "medium";
          updates.priority = priority;
          changes.push(`priority: ${prev} → ${priority}`);
        }
    
        if (name !== undefined && name !== existing.name) {
          updates.name = name;
          changes.push(`name updated`);
        }
    
        // description: null clears, string replaces, undefined leaves unchanged
        if (description !== undefined && description !== existing.description) {
          updates.description = description; // can be null or string
          changes.push(description === null ? `description cleared` : `description updated`);
        }
    
        // metadata: null clears, object shallow-merges, undefined leaves unchanged
        if (metadata !== undefined) {
          if (metadata === null) {
            // Only count as a change if there was actually metadata before
            if (existing.metadata && Object.keys(existing.metadata).length > 0) {
              updates.metadata = {};
              changes.push(`metadata cleared`);
            }
          } else {
            const merged = { ...(existing.metadata ?? {}), ...metadata };
            // Skip the change when nothing actually differs after merge.
            // Uses order-independent deepEqual to avoid false-positives from
            // key reordering (V8 auto-sorts numeric keys, spread order can shift).
            if (!deepEqual(merged, existing.metadata ?? {})) {
              updates.metadata = merged;
              changes.push(`metadata merged`);
            }
          }
        }
    
        if (changes.length === 0) {
          return {
            content: [
              {
                type: "text" as const,
                text: JSON.stringify(
                  {
                    id: project_id,
                    name: existing.name ?? null,
                    status: existing.status ?? null,
                    message: "No changes to apply",
                  },
                  null,
                  2
                ),
              },
            ],
          };
        }
    
        // Bump updated_at only when there are real changes
        const now = Timestamp.now();
        updates.updated_at = now;
    
        await docRef.update(updates);
    
        // Mirror tasks.ts: write an activity_log entry for audit trail
        await db.collection("activity_log").add({
          task_id: null,
          session_id: null,
          agent_name: "system",
          action: "updated",
          details: `Project ${project_id}: ${changes.join(", ")}`,
          metadata: { project_id },
          created_at: now,
        });
    
        return {
          content: [
            {
              type: "text" as const,
              text: JSON.stringify(
                {
                  id: project_id,
                  name: (updates.name as string | undefined) ?? existing.name ?? null,
                  status: (updates.status as string | undefined) ?? existing.status ?? null,
                  changes,
                  message: `Project updated: ${changes.join(", ")}`,
                },
                null,
                2
              ),
            },
          ],
        };
      }
    );
  • deepEqual helper used by the handler for order-independent deep equality comparison of metadata objects, preventing spurious 'changed' events when key order differs.
    function deepEqual(a: unknown, b: unknown): boolean {
      if (a === b) return true;
      if (a === null || b === null) return a === b;
      if (typeof a !== "object" || typeof b !== "object") return false;
    
      if (Array.isArray(a) || Array.isArray(b)) {
        if (!Array.isArray(a) || !Array.isArray(b)) return false;
        if (a.length !== b.length) return false;
        for (let i = 0; i < a.length; i++) {
          if (!deepEqual(a[i], b[i])) return false;
        }
        return true;
      }
    
      const aKeys = Object.keys(a as Record<string, unknown>);
      const bKeys = Object.keys(b as Record<string, unknown>);
      if (aKeys.length !== bKeys.length) return false;
      for (const k of aKeys) {
        if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
        if (!deepEqual(
          (a as Record<string, unknown>)[k],
          (b as Record<string, unknown>)[k]
        )) return false;
      }
      return true;
    }
Behavior4/5

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

With no annotations, the description carries the full burden. It discloses behavioral traits like valid status transitions (active→paused, etc.) and the effect of passing null to description/metadata (clearing them). It also implies that archiving removes clutter from the active list. However, it doesn't mention permissions, reversibility of actions, or whether updates trigger side effects.

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 three sentences, front-loaded with the main action. The first sentence states what it does, the second gives usage scenarios, and the third clarifies null handling. No extraneous information, every sentence earns its place.

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 6 parameters (1 required) and no output schema, the description covers the key functionality and clarifies important behaviors like status transitions and null semantics. However, it doesn't mention that project_id is required (though the schema lists it) or describe the response after a successful update. Still, it provides enough context for the agent to use the tool effectively.

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?

Schema coverage is 100% with detailed descriptions for each parameter. The tool description adds minimal extra nuance beyond summarizing the purpose. It reiterates the null behavior for description and metadata, but the schema already covers that. Therefore, the description does not significantly enhance parameter understanding beyond the schema.

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 clearly states it updates a project's status, priority, name, description, or metadata. It distinguishes from sibling tools by focusing on project-specific fields and providing concrete examples like pausing, archiving, and reranking priority.

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 gives explicit usage scenarios: pausing projects on hold, archiving completed ones, and reranking priority during weekly reviews. While it doesn't explicitly list when not to use or directly compare with siblings, the context is clear and actionable.

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