Skip to main content
Glama

bulk_modify

DestructiveIdempotent

Bulk-apply label changes to Gmail messages matching a query, batching up to 1000 per request. Supports dry run, verification, and undo for mass archiving, labeling, or mark-read.

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. A dry run that matches anything also returns previewToken: the server keeps the exact matched message set for 30 minutes. Pass it back with the SAME query, add and remove and the write acts on exactly that set — mail that started matching after the preview is left alone and counted in fromPreview.newMatches, and a reworded query or a different label change is refused before anything is written. A token works once; without one the call re-runs the query as before. 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, then the write with the dry run's previewToken. 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.

Tool call template

Replace each placeholder with a value matching the input schema. Only required parameters are included; add optional parameters as needed.

{
  "id": 1,
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "arguments": {
      "query": "<query>"
    },
    "name": "bulk_modify"
  }
}

Input Schema

NameRequiredDescriptionDefault
addNo
undoNo
queryYes
dryRunNo
removeNo
verifyNo
crossCheckNo
maxMessagesNo
previewTokenNo

Output Schema

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

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed5 schema fields changedv0.25.0
    • removedInput schema / additionalProperties
      Removed value: -false
    • addedInput schema / properties / previewToken
      Added value: +{
      +  "minLength": 1,
      +  "type": "string"
      +}
    • addedOutput schema / properties / fromPreview
      Added value: +{
      +  "additionalProperties": false,
      +  "properties": {
      +    "capped": {
      +      "type": "boolean"
      +    },
      +    "heldForSeconds": {
      +      "type": "number"
      +    },
      +    "heldMessages": {
      +      "type": "number"
      +    },
      +    "newMatches": {
      +      "type": "number"
      +    },
      +    "noLongerMatching": {
      +      "type": "number"
      +    }
      +  },
      +  "required": [
      +    "heldMessages",
      +    "heldForSeconds",
      +    "newMatches",
      +    "noLongerMatching",
      +    "capped"
      +  ],
      +  "type": "object"
      +}
    • addedOutput schema / properties / previewExpiresInSeconds
      Added value: +{
      +  "type": "number"
      +}
    • addedOutput schema / properties / previewToken
      Added value: +{
      +  "type": "string"
      +}
  2. 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"
      +}
  3. 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"
      +}
  4. 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"
      +]
  5. 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"
      +]
  6. Changed1 schema field changedv0.6.0
    • addedInput schema / properties / query / minLength
      Added value: +1
  7. 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"
      +]
  8. First observedv0.1.8

TDQS

A4.8/5.0
Behavior5/5

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

Despite rich annotations, the description adds extensive behavioral context: partial success reporting, the critical distinction between submitted and actually changed messages, 204/no-body API behavior, verification cost and semantics, dry-run index limitations, undo plan failure modes, and side effects. It also correctly describes destructive and reversible behavior without contradicting the annotations.

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

Conciseness3/5

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

The content is front-loaded and each major behavior is covered, but the description is extremely dense and very long, with experimental measurement detail that borders on over-explanation. It would benefit from tighter wording or structure, though most of the information is substantive for this complex tool.

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?

Given nine parameters, destructive writes, complex preview/verify/undo modes, and an existing output schema, the description is complete enough for correct invocation. It covers error conditions, result-field semantics, capping behavior, and safe usage patterns.

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 for all nine parameters. It explains add/remove label-name creation and ignoring, maxMessages capping, dryRun rehearsal semantics, previewToken single-use behavior, verify, crossCheck, and undo in detail beyond the bare 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 opens with a specific verb and resource: bulk-apply label changes to every message matching a Gmail query. It distinguishes this from single-thread tools and names modify_labels as the alternative for individual threads.

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?

It gives explicit USE WHEN examples, a DO NOT USE exclusion for single threads or empty add/remove, recommends dryRun first for broad queries, and distinguishes when to use verify, crossCheck, undo, and previewToken. Alternatives and preconditions are all stated.

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