Source: https://cli.nylas.com/guides/gmail-categories-api

# 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](https://cli.nylas.com/authors/nick-barraclough) Product Manager

Updated June 6, 2026

> **TL;DR:** Gmail's 5 inbox tabs are system labels: `CATEGORY_PERSONAL`, `CATEGORY_SOCIAL`, `CATEGORY_PROMOTIONS`, `CATEGORY_UPDATES`, and `CATEGORY_FORUMS`. The `labelIds` filter requires a match on *all* listed labels, so pass one category at a time, or use `q="category:promotions OR category:social"`. From the terminal: `nylas email list --folder CATEGORY_PROMOTIONS`.

> **Disclosure:** Nylas CLI is built by Nylas, Inc. This comparison reflects our testing and product understanding as of June 6, 2026.

Command references used in this guide: [`nylas email list`](https://cli.nylas.com/docs/commands/email-list) and [`nylas email folders list`](https://cli.nylas.com/docs/commands/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](https://gmail.googleblog.com/2013/05/a-new-inbox-that-puts-you-back-in.html). The [Gmail API labels guide](https://developers.google.com/workspace/gmail/api/guides/labels) 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 ID | Inbox tab | Search operator |
| --- | --- | --- |
| CATEGORY_PERSONAL | Primary | category:primary |
| CATEGORY_SOCIAL | Social | category:social |
| CATEGORY_PROMOTIONS | Promotions | category:promotions |
| CATEGORY_UPDATES | Updates | category:updates |
| CATEGORY_FORUMS | Forums | category: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](https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.messages/list) 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.

```python
# 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](https://support.google.com/mail/answer/7190?hl=en) 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.

```python
# 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](https://developers.google.com/workspace/gmail/api/guides/labels), 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](https://cli.nylas.com/guides/gmail-labels-api). 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.

```bash
# 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`](https://cli.nylas.com/docs/commands/email-folders-list) on any connected account to see which folder or label IDs are available before scripting against them.

## Next steps

- [Gmail Labels API](https://cli.nylas.com/guides/gmail-labels-api) — create, modify, and batch-apply labels, including the modify scope
- [Gmail API search queries](https://cli.nylas.com/guides/gmail-api-search-query) — the full q operator syntax that category: composes with
- [Gmail API: list spam and trash](https://cli.nylas.com/guides/gmail-api-spam-trash) — the other hidden-by-default system labels
- [List Gmail emails from the terminal](https://cli.nylas.com/guides/list-gmail-emails) — the base CLI listing workflow these tab filters build on
- [Full command reference](https://cli.nylas.com/docs/commands) — every flag and subcommand documented
