Skip to main content
Glama

bulk_modify

DestructiveIdempotent

Bulk-apply label changes to every Gmail message matching a query, with dry run, verification, and undo for safe reversible mass edits.

Instructions

Bulk-apply label changes to every message matching a Gmail query, batched at 1000 messages per API request. Labels may be given by name or by id: an unknown name in add is created automatically (use '/' for nested labels), an unknown name in remove is ignored. Returns matched/submitted counts, matched and submitted thread IDs (both lists capped at 500 — matchedThreadCount/submittedThreadCount hold the true totals), and per-chunk failures (partial success is reported, not hidden). IMPORTANT: submittedMessages is how many ids were handed to the API, NOT how many messages changed — messages.batchModify answers 204 with no body and ignores ids it does not recognise without a word, so an accepted request is not a performed one. Set verify:true to read the labels back afterwards and get verified {applied, notApplied[], unverifiable[]} — the only field here that reports an observed outcome. It costs one extra read per affected thread, so it is off by default; use it when a wrong 'done' would be acted on (trashing, or anything the user is told completed). If more messages match than maxMessages, only the first maxMessages are processed and 'capped' is true — raise maxMessages or re-run to finish the rest. NOTE: the query hits Gmail's search index as-is, WITHOUT the live re-verification search performs. The staleness that makes search re-verify was measured on threads.list (132 threads returned, 114 carrying no unread message at all); the same query through the message index this tool uses returned 19 hits, none stale — same mailbox, same minute. So the known drift does not reach this path, but that is one measurement, not a guarantee: unverifiedPredicates in the result names the conditions taken on the index's word, and when the outcome must be read-state-precise, resolve the set with search (which verifies against live labels) and act on those thread ids instead. Set crossCheck:true to ask Gmail the same question a second way before writing: each derived predicate is re-run as a label filter (labelIds) instead of a query operator, and any message the two routes disagree about is left untouched and listed in crossChecked.dropped. It costs one extra list per predicate — flat, not per message — so unlike verify it stays cheap on a large sweep. Read it as a contradiction detector: a disagreement is real, agreement proves nothing, because both routes read the same index. unverifiedPredicates therefore stays as it is even when this runs. A capped match set is not cross-checked at all (crossChecked.capped), since a message missing from a page is not a message missing the label. Set dryRun:true to rehearse: the same query resolution, matched counts/threads and the labels that would be created — and no message or label is touched. A dry run reads the SAME unverified index, so it confirms the size of the set, never its correctness. USE WHEN: mass operations — 'archive all newsletters older than 30 days' (query + remove INBOX), bulk labeling, bulk mark-read; dryRun first when the query is broad or the user should see the set before it changes. DO NOT USE: for a single thread (use modify_labels or the dedicated tools), or with neither add nor remove. Set undo:true to get undo, the EXACT inverse of this call: before writing, every affected thread's labels are read live, and the plan says per message what to add back and what to take off — a message that was already in the target state is left out, which a swapped add/remove would not do. Pass undo.steps unchanged to undo_bulk_modify to revert. It costs one extra read per affected thread. With undo:true the call writes NOTHING unless the plan can be built: more than 500 messages to change, or a prior state that could not be read, is an error before the first write — a change the caller asked to be reversible is never made irreversibly. A label the call created for a name in add is taken off again by the undo but stays in the mailbox, empty. SIDE EFFECTS: modifies up to maxMessages messages in one call (none with dryRun); label changes are reversible — exactly with undo:true, approximately by the inverse call.

Input Schema

NameRequiredDescriptionDefault
addNo
undoNo
queryYes
dryRunNo
removeNo
verifyNo
crossCheckNo
maxMessagesNo

Output Schema

NameRequiredDescriptionDefault
undoNo
cappedYes
dryRunYes
failedYes
verifiedNo
crossCheckedNo
labelsToCreateNo
matchedThreadsYes
matchedMessagesYes
submittedThreadsYes
submittedMessagesYes
matchedThreadCountYes
submittedThreadCountYes
unverifiedPredicatesYes

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed2 schema fields changedv0.23.0
    • addedInput schema / properties / undo
      Added value: +{
      +  "default": false,
      +  "type": "boolean"
      +}
    • addedOutput schema / properties / undo
      Added value: +{
      +  "additionalProperties": false,
      +  "properties": {
      +    "steps": {
      +      "items": {
      +        "additionalProperties": false,
      +        "properties": {
      +          "add": {
      +            "items": {
      +              "type": "string"
      +            },
      +            "type": "array"
      +          },
      +          "messageIds": {
      +            "items": {
      +              "type": "string"
      +            },
      +            "type": "array"
      +          },
      +          "remove": {
      +            "items": {
      +              "type": "string"
      +            },
      +            "type": "array"
      +          },
      +          "threadIds": {
      +            "items": {
      +              "type": "string"
      +            },
      +            "type": "array"
      +          }
      +        },
      +        "required": [
      +          "add",
      +          "remove",
      +          "messageIds",
      +          "threadIds"
      +        ],
      +        "type": "object"
      +      },
      +      "type": "array"
      +    },
      +    "unknown": {
      +      "items": {
      +        "type": "string"
      +      },
      +      "type": "array"
      +    }
      +  },
      +  "required": [
      +    "steps",
      +    "unknown"
      +  ],
      +  "type": "object"
      +}
  2. Changed2 schema fields changedv0.18.0
    • addedInput schema / properties / crossCheck
      Added value: +{
      +  "default": false,
      +  "type": "boolean"
      +}
    • addedOutput schema / properties / crossChecked
      Added value: +{
      +  "additionalProperties": false,
      +  "properties": {
      +    "capped": {
      +      "type": "boolean"
      +    },
      +    "dropped": {
      +      "items": {
      +        "additionalProperties": false,
      +        "properties": {
      +          "id": {
      +            "type": "string"
      +          },
      +          "predicate": {
      +            "type": "string"
      +          },
      +          "threadId": {
      +            "type": "string"
      +          }
      +        },
      +        "required": [
      +          "id",
      +          "threadId",
      +          "predicate"
      +        ],
      +        "type": "object"
      +      },
      +      "type": "array"
      +    },
      +    "droppedMessages": {
      +      "type": "number"
      +    },
      +    "predicates": {
      +      "items": {
      +        "type": "string"
      +      },
      +      "type": "array"
      +    },
      +    "skipped": {
      +      "items": {
      +        "type": "string"
      +      },
      +      "type": "array"
      +    }
      +  },
      +  "required": [
      +    "predicates",
      +    "droppedMessages",
      +    "dropped",
      +    "capped"
      +  ],
      +  "type": "object"
      +}
  3. Changed9 schema fields changedv0.15.1
    • addedInput schema / properties / verify
      Added value: +{
      +  "default": false,
      +  "type": "boolean"
      +}
    • removedOutput schema / properties / modifiedMessages
      Removed value: -{
      -  "type": "number"
      -}
    • removedOutput schema / properties / modifiedThreadCount
      Removed value: -{
      -  "type": "number"
      -}
    • removedOutput schema / properties / modifiedThreads
      Removed value: -{
      -  "items": {
      -    "type": "string"
      -  },
      -  "type": "array"
      -}
    • addedOutput schema / properties / submittedMessages
      Added value: +{
      +  "type": "number"
      +}
    • addedOutput schema / properties / submittedThreadCount
      Added value: +{
      +  "type": "number"
      +}
    • addedOutput schema / properties / submittedThreads
      Added value: +{
      +  "items": {
      +    "type": "string"
      +  },
      +  "type": "array"
      +}
    • addedOutput schema / properties / verified
      Added value: +{
      +  "additionalProperties": false,
      +  "properties": {
      +    "applied": {
      +      "type": "number"
      +    },
      +    "notApplied": {
      +      "items": {
      +        "type": "string"
      +      },
      +      "type": "array"
      +    },
      +    "unverifiable": {
      +      "items": {
      +        "type": "string"
      +      },
      +      "type": "array"
      +    }
      +  },
      +  "required": [
      +    "applied",
      +    "notApplied",
      +    "unverifiable"
      +  ],
      +  "type": "object"
      +}
    • changedOutput schema / required
      Previous value: -[
      -  "dryRun",
      -  "matchedMessages",
      -  "matchedThreadCount",
      -  "matchedThreads",
      -  "modifiedMessages",
      -  "modifiedThreadCount",
      -  "modifiedThreads",
      -  "capped",
      -  "unverifiedPredicates",
      -  "failed"
      -]New value: +[
      +  "dryRun",
      +  "matchedMessages",
      +  "matchedThreadCount",
      +  "matchedThreads",
      +  "submittedMessages",
      +  "submittedThreadCount",
      +  "submittedThreads",
      +  "capped",
      +  "unverifiedPredicates",
      +  "failed"
      +]
  4. Changed7 schema fields changedv0.11.0
    • addedInput schema / properties / dryRun
      Added value: +{
      +  "default": false,
      +  "type": "boolean"
      +}
    • addedOutput schema / properties / dryRun
      Added value: +{
      +  "type": "boolean"
      +}
    • addedOutput schema / properties / labelsToCreate
      Added value: +{
      +  "items": {
      +    "type": "string"
      +  },
      +  "type": "array"
      +}
    • addedOutput schema / properties / matchedThreadCount
      Added value: +{
      +  "type": "number"
      +}
    • addedOutput schema / properties / matchedThreads
      Added value: +{
      +  "items": {
      +    "type": "string"
      +  },
      +  "type": "array"
      +}
    • addedOutput schema / properties / unverifiedPredicates
      Added value: +{
      +  "items": {
      +    "type": "string"
      +  },
      +  "type": "array"
      +}
    • changedOutput schema / required
      Previous value: -[
      -  "matchedMessages",
      -  "modifiedMessages",
      -  "modifiedThreadCount",
      -  "modifiedThreads",
      -  "capped",
      -  "failed"
      -]New value: +[
      +  "dryRun",
      +  "matchedMessages",
      +  "matchedThreadCount",
      +  "matchedThreads",
      +  "modifiedMessages",
      +  "modifiedThreadCount",
      +  "modifiedThreads",
      +  "capped",
      +  "unverifiedPredicates",
      +  "failed"
      +]
  5. Changed1 schema field changedv0.6.0
    • addedInput schema / properties / query / minLength
      Added value: +1
  6. Changed2 schema fields changedv0.1.9
    • addedOutput schema / properties / capped
      Added value: +{
      +  "type": "boolean"
      +}
    • changedOutput schema / required
      Previous value: -[
      -  "matchedMessages",
      -  "modifiedMessages",
      -  "modifiedThreadCount",
      -  "modifiedThreads",
      -  "failed"
      -]New value: +[
      +  "matchedMessages",
      +  "modifiedMessages",
      +  "modifiedThreadCount",
      +  "modifiedThreads",
      +  "capped",
      +  "failed"
      +]
  7. First observedv0.1.8

TDQS

A4.9/5.0
Behavior5/5

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

Goes far beyond the annotations (readOnly=false, destructive=true, idempotent=true): it discloses partial-success reporting, the 204-no-body behavior and that submittedMessages != messages changed, per-thread cost of verify/undo, the pre-write abort semantics of undo:true, the flat cost of crossCheck, and that capped sets are not cross-checked. This is exactly the kind of context annotations cannot carry.

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

Conciseness4/5

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

The opening sentence is front-loaded and the USE WHEN / DO NOT USE markers aid scanning, but the body is a very long wall of text. Most sentences earn their place given the tool's complexity, though the staleness-measurement digression (132 threads / 114 unread / 19 hits) is longer than strictly needed to justify unverifiedPredicates.

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

Completeness5/5

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

For an 8-parameter, multi-mode, destructive bulk tool with rich semantics, the description is genuinely complete: it covers every mode, the precedence and cost of each flag, failure/partial-success behavior, and the index-staleness caveat. An output schema exists, yet the extra return-shape context given (verified, crossChecked.dropped, undo.steps) is useful rather than redundant.

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

Parameters5/5

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

Schema description coverage is 0%, so the description carries the full burden and does so: it explains add (name or id, unknown name created, '/' for nested), remove (unknown name ignored), maxMessages (capping + 'capped' flag), dryRun, verify, crossCheck, and undo. Every one of the 8 parameters gains meaning beyond its type/default.

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?

Opens with a specific verb+resource+scope: 'Bulk-apply label changes to every message matching a Gmail query, batched at 1000 messages per API request.' It clearly distinguishes itself from near-siblings like modify_labels (single-thread) and the dedicated tools, so an agent can route correctly without opening a schema.

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

Usage Guidelines5/5

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

Contains explicit USE WHEN ('mass operations... archive all newsletters older than 30 days') and DO NOT USE ('for a single thread (use modify_labels or the dedicated tools), or with neither add nor remove') sections, plus conditional guidance on when to prefer dryRun/verify/undo/crossCheck. Alternatives are named directly.

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