Guide

Gmail API Categories: Tabs, Labels, and Search

Gmail has been sorting inboxes into tabs since May 2013, but the API surface behind those tabs trips up developers in a specific way: filtering messages.list by two category labels at once returns nothing, because labelIds is an AND filter, not an OR. This guide maps the 5 CATEGORY_* labels to their tabs and search operators, explains the match-all gotcha, and shows how to read any tab from the API or a single CLI flag.

Written by Nick Barraclough Product Manager

VerifiedCLI 3.1.16 · Gmail · last tested June 6, 2026

Command references used in this guide: nylas email list and nylas email folders list.

What are Gmail category labels?

A Gmail category label is a system label that mirrors one of the inbox tabs Google introduced in May 2013. The Gmail API labels guide lists 5 of them, each described as corresponding to "messages that are displayed in the [tab name] tab of the Gmail interface." All 5 can be manually applied through the API.

Label IDInbox tabSearch operator
CATEGORY_PERSONALPrimarycategory:primary
CATEGORY_SOCIALSocialcategory:social
CATEGORY_PROMOTIONSPromotionscategory:promotions
CATEGORY_UPDATESUpdatescategory:updates
CATEGORY_FORUMSForumscategory:forums

Note the naming mismatch on the first row: the Primary tab's label is CATEGORY_PERSONAL, not CATEGORY_PRIMARY. Scripts that guess the label name from the tab name fail on exactly that one. Gmail's classifier assigns these labels automatically as mail arrives; your code mostly reads them.

Why does labelIds with two categories return nothing?

The labelIds parameter on messages.list is an AND filter. The API reference says it returns only messages "with labels that match all of the specified label IDs." Passing ["CATEGORY_PROMOTIONS", "CATEGORY_SOCIAL"] therefore asks for messages carrying both category labels at once, and since the classifier files each incoming message under one tab, the query comes back empty.

To read two tabs in one request, move the condition into the q parameter, which supports Gmail search syntax including OR. The 3 queries below show the failing AND filter, the working OR search, and the single-category form that works either way.

# WRONG: AND semantics — promotions AND social on the same message
service.users().messages().list(
    userId="me",
    labelIds=["CATEGORY_PROMOTIONS", "CATEGORY_SOCIAL"],
).execute()  # → {} (no 'messages' key)

# RIGHT: OR across two tabs via the q parameter
service.users().messages().list(
    userId="me",
    q="category:promotions OR category:social",
).execute()

# Single tab: labelIds works fine with one ID
service.users().messages().list(
    userId="me",
    labelIds=["CATEGORY_PROMOTIONS"],
    maxResults=100,
).execute()

The same AND semantics make labelIds=["CATEGORY_PROMOTIONS", "UNREAD"] useful rather than broken: category plus state labels do co-exist on one message, so that pair returns unread promotions. The rule of thumb is one category label per request, combined freely with non-category labels. Each list call costs 5 quota units regardless of how many labels you pass.

How do category: search operators differ from the labels?

Gmail's search syntax exposes 7 category operators, 2 more than the API's 5 category labels. Google's search operators reference lists category:primary, category:social, category:promotions, category:updates, category:forums, plus category:reservations and category:purchases. The last 2 have no CATEGORY_* label ID, so search is the only way to query them.

That asymmetry matters for receipt and travel automation. A script that extracts order confirmations can run q="category:purchases newer_than:30d" and let Google's classifier do the entity detection, but it can't get the same set from a labelIds filter. Combine operators to narrow further; every operator from the search box works in the API's q parameter unchanged.

# Order confirmations from the last 30 days
q = "category:purchases newer_than:30d"

# Promotions with attachments from one sender
q = "category:promotions from:@retailer.example has:attachment"

# Reservations (flights, hotels) this quarter
q = "category:reservations after:2026/04/01"

Can you change a message's category through the API?

Yes. All 5 CATEGORY_* labels are listed as manually applicable in the labels guide, so a messages.modify call that adds one category and removes another moves the message between tabs. Bulk moves use batchModify, which covers up to 1,000 messages for 50 quota units.

The mechanics of modify and batchModify, including the gmail.modify scope they require, are covered in the Gmail Labels API guide. The category-specific detail: removing a category label without adding a replacement lands the message in Primary, so tab moves should always pair one addLabelIds with one removeLabelIds.

How do you list category tabs from the CLI?

The nylas email list --folder CATEGORY_PROMOTIONS command reads a Gmail tab directly, with no OAuth client setup or Python dependency. The --folder flag takes any of the 5 category label IDs, and --json output pipes into jq for counting, grouping, or extracting senders. These commands were verified against a live Gmail account on CLI 3.1.16.

# Read the Promotions tab
nylas email list --folder CATEGORY_PROMOTIONS --limit 20

# Count unread messages in the Social tab
nylas email list --folder CATEGORY_SOCIAL --unread --json | jq length

# Top senders in Updates — find subscription noise
nylas email list --folder CATEGORY_UPDATES --json --limit 100 | \
  jq -r '.[].from[0].email' | sort | uniq -c | sort -rn | head -10

Category labels are Gmail-only, so on Outlook or IMAP accounts the equivalent grouping lives in folders like Junk Email or user-created ones. Run nylas email folders list on any connected account to see which folder or label IDs are available before scripting against them.

Next steps