# RPC & UI Reference **Status:** Active **Last Updated:** 2026-06-17 **Source of Truth:** `src/notebooklm/rpc/types.py` for method IDs; payload builders in `src/notebooklm/` and golden tests under `tests/unit/` **Purpose:** Complete reference for RPC methods, UI selectors, and payload structures > **Note:** Payload structures are extracted from the implementation builders in > `src/notebooklm/` and pinned by golden unit tests. Each payload includes a > reference to its owning source file. The CREATE_ARTIFACT payloads below were > re-verified against the live builders in `_artifact/payloads.py` on > 2026-06-11 (AUDIO, VIDEO_EXPLAINER, VIDEO_BRIEF, VIDEO_CINEMATIC, > STUDY_GUIDE, BRIEFING_DOC, BLOG_POST, MIND_MAP, QUIZ, FLASHCARDS, > INFOGRAPHIC, SLIDE_DECK, DATA_TABLE). Read-only notebook/source/artifact/chat/ > sharing/settings RPCs were live-captured again on 2026-06-15. Home, Sources, > chat, and Studio selectors were rechecked the same day against a live Chrome > session on a probe notebook. --- ## Quick Reference ### RPC Method Status | RPC ID | Method | Purpose | Implementation | |--------|--------|---------|----------------| | `wXbhsf` | LIST_NOTEBOOKS | List all notebooks | `_notebooks.py` | | `CCqFvf` | CREATE_NOTEBOOK | Create new notebook | `_notebooks.py` | | `rLM1Ne` | GET_NOTEBOOK | Get notebook details + sources | `_notebooks.py` | | `s0tc2d` | RENAME_NOTEBOOK | Rename, chat config, share access | `_notebooks.py`, `_chat/api.py` | | `WWINqb` | DELETE_NOTEBOOK | Delete a notebook | `_notebooks.py` | | `izAoDd` | ADD_SOURCE | Add URL/text/YouTube/Drive source | `_source/add.py` via `_sources.py` | | `o4cbdc` | ADD_SOURCE_FILE | Register uploaded file (PDF, DOCX, EPUB, etc.) | `_source/upload.py`, `_source/upload_payloads.py` | | `tGMBJ` | DELETE_SOURCE | Delete a source | `_sources.py` | | `b7Wfje` | UPDATE_SOURCE | Rename source | `_sources.py` | | `tr032e` | GET_SOURCE_GUIDE | Get source summary | `_sources.py` | | `hizoJc` | GET_SOURCE | Get clean fulltext content of a source | `_source/content.py` | | `agX4Bc` | CREATE_LABEL | AI-generate label groupings and create manual labels | `_labels.py` | | `I3xc3c` | LIST_LABELS | List source labels for a notebook | `_labels.py` | | `le8sX` | UPDATE_LABEL | Rename label, set emoji, add/remove sources | `_labels.py`, `_label/params.py` | | `GyzE7e` | DELETE_LABEL | Delete one or more labels (batch) | `_labels.py` | | `R7cb6c` | CREATE_ARTIFACT | Unified artifact generation | `_artifacts.py` | | `gArtLc` | LIST_ARTIFACTS | List artifacts in a notebook | `_artifacts.py` | | `V5N4be` | DELETE_ARTIFACT | Delete artifact | `_artifacts.py` | | `KmcKPe` | REVISE_SLIDE | Revise an individual slide via prompt | `_artifacts.py` | | `Rytqqe` | RETRY_ARTIFACT | Retry a failed Studio artifact in place | `_artifacts.py` | | `hPTbtc` | GET_LAST_CONVERSATION_ID | Get most recent conversation ID | `_chat/api.py` | | `khqZz` | GET_CONVERSATION_TURNS | Get Q&A turns for a conversation | `_chat/api.py` | | `J7Gthc` | DELETE_CONVERSATION | Delete a conversation (web UI's "Delete history") | `_chat/api.py` | | `otmP3b` | SUGGEST_PROMPTS | Get AI-suggested prompts for a notebook | `_notebooks.py` | | `CYK0Xb` | CREATE_NOTE | Create a note (placeholder) | `_notes.py` | | `cYAfTb` | UPDATE_NOTE | Update note content/title | `_notes.py` | | `AH0mwd` | DELETE_NOTE | Delete a note | `_notes.py` | | `cFji9` | GET_NOTES_AND_MIND_MAPS | List notes and mind maps | `_notes.py` | | `yyryJe` | GENERATE_MIND_MAP | Mind map generation | `_artifacts.py` | | `VfAZjd` | SUMMARIZE | Get notebook summary | `_notebooks.py` | | `FLmJqe` | REFRESH_SOURCE | Refresh URL/Drive source | `_sources.py` | | `yR9Yof` | CHECK_SOURCE_FRESHNESS | Check if source needs refresh | `_sources.py` | | `Ljjv0c` | START_FAST_RESEARCH | Start fast research | `_research.py` | | `QA9ei` | START_DEEP_RESEARCH | Start deep research | `_research.py` | | `e3bVqc` | POLL_RESEARCH | Poll research status | `_research.py` | | `LBwxtb` | IMPORT_RESEARCH | Import research results | `_research.py` | | `Zbrupe` | CANCEL_RESEARCH | Cancel in-flight research run | `_research.py` | | `rc3d8d` | RENAME_ARTIFACT | Rename artifact | `_artifacts.py` | | `Krh3pd` | EXPORT_ARTIFACT | Export to Docs/Sheets | `_artifacts.py` | | `RGP97b` | SHARE_ARTIFACT | Legacy notebook/artifact share-link toggle | `_sharing_manager.py` | | `QDyure` | SHARE_NOTEBOOK | Set notebook visibility (restricted/public) | `_sharing.py` | | `JFMDGd` | GET_SHARE_STATUS | Get notebook share settings | `_sharing.py` | | `ciyUvf` | GET_SUGGESTED_REPORTS | Get AI-suggested report formats | `_artifacts.py` | | `v9rmvd` | GET_INTERACTIVE_HTML | Fetch quiz/flashcard HTML (`[0][9][0]`) / interactive mind-map tree (`[0][9][3]`) | `_artifact/downloads.py` | | `fejl7e` | REMOVE_RECENTLY_VIEWED | Remove notebook from recent list | `_notebooks.py` | | `ZwVcOc` | GET_USER_SETTINGS | Get user settings including output language | `_settings.py` | | `hT54vc` | SET_USER_SETTINGS | Set user settings (e.g., output language) | `_settings.py` | ### Content Type Codes (ArtifactTypeCode) | Code | Type | Used By | |------|------|---------| | 1 | Audio | Audio Overview | | 2 | Report | Briefing Doc, Study Guide, Blog Post | | 3 | Video | Video Overview | | 4 | Quiz/Flashcards (QUIZ_FLASHCARD alias) | Quiz (variant=2), Flashcards (variant=1) | | 5 | Mind Map | Library synthetic type for note-backed mind maps | | 7 | Infographic | Infographic | | 8 | Slide Deck | Slide Deck | | 9 | Data Table | Data Table | ### Source Type Codes (file uploads & sources) Internal integer codes returned by `GET_NOTEBOOK` / `LIST_SOURCES` and consumed by `Source.from_api_response()` (mapped to `SourceType` in `src/notebooklm/types.py`). | Code | `SourceType` | Used By | |------|--------------|---------| | 1 | `GOOGLE_DOCS` | Google Docs source | | 2 | `GOOGLE_SLIDES` | Google Slides source | | 3 | `PDF` | PDF upload | | 4 | `PASTED_TEXT` | Inline pasted text | | 5 | `WEB_PAGE` | Web URL source | | 8 | `MARKDOWN` | Markdown file | | 9 | `YOUTUBE` | YouTube URL | | 10 | `MEDIA` | Audio / video upload | | 11 | `DOCX` | Word document | | 13 | `IMAGE` | Image upload | | 14 | `GOOGLE_SPREADSHEET` | Google Sheets source **and** Drive-hosted binaries (see overload note) | | 16 | `CSV` | CSV upload | | 17 | `EPUB` | EPUB upload (added in v0.4.0) | > Codes outside this map are surfaced as `SourceType.UNKNOWN` and emit `UnknownTypeWarning` on first occurrence so unmapped types don't crash callers. > **Code `14` is overloaded** (live-captured #1828/#1832): the backend returns `14` for a native Google Sheet *and* for a Drive-hosted PDF. Drive sources carry no URL (`metadata[0]/[5]/[7]` are all null), so the two are disambiguated by the MIME at `metadata[19]` (fallback `metadata[9][2]`): `application/vnd.google-apps.spreadsheet` → `GOOGLE_SPREADSHEET`, `application/pdf` → `PDF`. See `_disambiguate_type_code` in `src/notebooklm/_types/sources.py`. --- ## UI/Library Operation Parity Use this table as the coverage index before adding or changing selectors. "UI covered" means the selector or flow is documented below from the 2026-06-15 live Chrome probe. "Library-only" means the Python API deliberately exposes a backend or local convenience that has no stable web-control equivalent in the capture. | Library surface | UI parity status | Notes | |-----------------|------------------|-------| | `NotebooksAPI.list/create/get/rename/delete/remove_from_recent` | Partial UI coverage | Home create/card/action-menu selectors are covered. Rename/delete/remove-recent are represented by project action menus and RPC payloads; destructive menu items were not re-mutated in the live probe. | | `NotebooksAPI.get_summary/get_description/get_metadata/get_raw/get_share_url` | Library-only/read-derived | Summary content is visible in the chat panel, but these are read/format helpers rather than direct UI controls. | | `SourcesAPI.list/get/add_url/add_text/add_file/add_drive/delete/rename` | UI covered | Source cards, add-source modal tabs, upload/Drive entry points, source menus, and submit selectors are documented. | | `SourcesAPI.get_guide/get_fulltext` | UI covered/read-derived | Opening a source exposes the source viewer, source guide toggle, title input, and source content; `get_fulltext()` is the programmatic extraction path. | | `SourcesAPI.wait_*`, `refresh`, `check_freshness` | Library-only/partial UI | Wait methods are polling helpers. Refresh/freshness RPCs are documented, but no stable refresh selector was captured in the current source-list/label-list state. | | `LabelsAPI.list/sources/generate/create/update/rename/set_emoji/add_sources/remove_sources/delete` | UI covered | Auto-label, Reorganize all sources, manual label creation, inline rename, emoji picker, delete, label panels, and source Move to label checkboxes are documented. | | `ChatAPI.ask/get_history/delete_conversation/configure/save_answer_as_note` | UI covered | Chat input/send, options/delete history, configure dialog, and `Save message to a note` buttons are documented. `get_conversation_id`, cache methods, and history parsing are backend/local conveniences. | | `ArtifactsAPI.generate_*`, `suggest_reports`, `list/get/get_prompt/delete/rename/share/export` | UI covered/partial | All live Studio generation tiles and option sets are documented. Artifact list/open/menu/view-prompt/share/delete selectors are covered; export/download/retry availability depends on artifact type/status. | | `ArtifactsAPI.download_*`, `wait_for_completion`, `poll_status`, `revise_slide`, `retry_failed` | Library-only/conditional UI | Downloads, polling, and slide revision are programmatic conveniences. Retry requires a failed artifact row; the RPC is documented but no failed-row retry selector was present in the probe. | | `NotesAPI.list/get/create/update/delete` | UI covered/partial | Add note, note row, note view close/title input, and note menu delete are documented. Rich body editing uses NotebookLM's internal editor; keep selectors conservative. | | `MindMapsAPI.list/generate/rename/delete/get_tree` | UI covered/partial | Interactive mind map generation is the live Studio tile. Note-backed mind maps are a synthetic/library backing; tree extraction via `GET_INTERACTIVE_HTML` is programmatic. | | `ResearchAPI.start/poll/wait/import_sources` | UI covered for start only | Source discovery corpus/mode/submit selectors map to fast/deep web/Drive research. Polling and import verification are backend workflow helpers. | | `SettingsAPI.get/set_output_language`, `SharingAPI.get_status/set_public/set_view_level/add_user/update_user/remove_user` | UI covered/partial | Settings and Share dialogs are covered at entry/save/copy selectors. Programmatic user-permission mutations go beyond the captured UI selectors. | | UI-only note operations | UI-only | Note menus expose `Convert to source`, `Convert all notes to source`, `Export to Docs`, and `Export to Sheets`; keep them documented as selectors unless/until a public library method owns those flows. | --- ## Using Selector Lists Selectors are provided as Python lists of **fallback options**. Try each in order: ```python async def try_selectors(page, selectors: list[str], action="click", timeout=5000): """Try multiple selectors until one works.""" for selector in selectors: try: element = page.locator(selector) if action == "click": await element.click(timeout=timeout) elif action == "fill": return element return True except Exception: continue raise Exception(f"None of the selectors worked: {selectors}") # Example usage await try_selectors(page, HOME_SELECTORS["create_notebook"]) ``` --- ## Home / Notebook List ### UI Selectors ```python HOME_SELECTORS = { "create_notebook": [ "button[aria-label='Create new notebook']", "button:has-text('Create new')", "mat-card[role='button']:has-text('Create new notebook')", ], "notebook_card": [ "mat-card:has(button[aria-label='Project Actions Menu'])", "mat-card:has(button:has-text('more_vert'))", "mat-card[role='button']:has(h3)", "a[href*='/notebook/']", ], "notebook_menu": [ "button[aria-label='Project Actions Menu']", "button[aria-label*='More options']", "button:has-text('more_vert')", ], } ``` ### Live Home Components (2026-06-15) ```python HOME_MENU_SELECTORS = { "settings": "button[aria-label='Settings']", "search": "button[aria-label='Open search']", "filter_tabs": [ "button:has-text('All')", "button:has-text('My notebooks')", "button:has-text('Featured notebooks')", ], "view_toggle": [ "button[aria-label='Grid view']", "button[aria-label='List view']", ], "sort_menu": ".project-filter-button", # e.g. "Most recent" "project_actions": "button[aria-label='Project Actions Menu']", } ``` The home project action menu is now labeled `Project Actions Menu`; older `More options` selectors did not match the live UI on 2026-06-15. ### RPC: LIST_NOTEBOOKS (wXbhsf) **Source:** `_notebooks.py::list()` ```python # Minimal client builder (`NotebooksAPI.list()`), accepted by the backend: params = [ None, # 0 1, # 1: Fixed value None, # 2 [2], # 3: Fixed flag ] # Live web UI/CDP capture on 2026-06-15: params = [ None, 1, None, [2, None, None, [1, None, None, None, None, None, None, None, None, None, [1]]], None, [[None, None, []], [[]], [None, []]], ] ``` ### RPC: CREATE_NOTEBOOK (CCqFvf) **Source:** `_notebooks.py::create()` ```python params = [ title, # 0: Notebook title None, # 1 None, # 2 [2, None, None, [1, None, None, None, None, None, None, None, None, None, [1]]], # 3: Shared request-options wrapper (`build_template_block()`) ] ``` ### RPC: DELETE_NOTEBOOK (WWINqb) **Source:** `_notebooks.py::delete()` ```python params = [ [notebook_id], # 0: Single-nested notebook ID [2], # 1: Fixed flag ] ``` ### RPC: GET_NOTEBOOK (rLM1Ne) **Source:** `_notebooks.py::get()`, `_source/listing.py::SourceLister.list()` ```python params = [ notebook_id, # 0 None, # 1 [2, None, None, [1, None, None, None, None, None, None, None, None, None, [1]]], # 2: Shared request-options wrapper None, # 3 0, # 4: Fixed value ] ``` The slot `[2]` wrapper replaced the older bare `[2]` read-path tail. Live capture on 2026-06-15 confirmed the nested shape in `f.req`; keep `_notebooks.build_get_notebook_params()` and `_source/listing.py` in sync. The live web UI also sends a sixth filter/tail slot: `[[None, None, []]]`. Initial page load used slot `[4] == 0`; a follow-up refresh used slot `[4] == 1`. The client builder currently omits that sixth slot because the backend accepts the compact form. ### RPC: REMOVE_RECENTLY_VIEWED (fejl7e) **Source:** `_notebooks.py::remove_from_recent()` Remove a notebook from the recently viewed list (doesn't delete the notebook). ```python params = [notebook_id] # Just the notebook ID # No source_path needed await rpc_call( RPCMethod.REMOVE_RECENTLY_VIEWED, params, allow_null=True, ) # Response: None (no return value) ``` --- ## Sources Panel ### UI Selectors ```python SOURCES_SELECTORS = { "add_sources": [ "button[aria-label='Add source']", "button:has-text('+ Add sources')", "button:has-text('Add sources')", ], "source_card": ".single-source-container", "source_menu": [ "button.source-item-more-button[aria-label='More']", "button[aria-label*='More options']", ], "remove_source": "button:has-text('Remove source')", "rename_source": "button:has-text('Rename source')", } ADD_SOURCE_MODAL = { "modal": "mat-dialog-container[role='dialog']", "upload_files": "button:has-text('Upload files')", "website_tab": [ "button:has-text('Websites')", "button:has-text('Website')", ], "drive_tab": "button:has-text('Drive')", "url_input": [ "textarea[aria-label='Enter URLs']", "textarea[placeholder='Paste any links']", "textarea[placeholder*='links']", ], "copied_text_tab": "button:has-text('Copied text')", "copied_text_input": [ "textarea[aria-label='Pasted text']", "textarea[placeholder='Paste text here']", ], "back_button": "button[aria-label='Back']", "close_button": "button[aria-label='Close']", "submit_button": "button:has-text('Insert')", } SOURCE_DISCOVERY_SELECTORS = { "query": [ "textarea[placeholder='Search the web for new sources']", "textarea[aria-label='Discover sources based on the inputted query']", ], "corpus_menu": ".corpus-menu-trigger", "corpus_web": ".corpus-option-web", "corpus_drive": ".corpus-option-drive", "research_mode_menu": ".researcher-menu-trigger", "fast_research": ".research-option-fast-research", "deep_research": ".research-option-deep-research", "submit": "button.actions-enter-button[aria-label='Submit']", "auto_label": [ "button[aria-label='Auto-label your sources by topic']", "button[aria-label='Undo or re-label sources']", ], } DRIVE_PICKER_SELECTORS = { "picker_iframe": "iframe[src*='picker'], iframe[src*='docs.google']", } SOURCE_VIEWER_SELECTORS = { "open_source": "button.source-stretched-button", "viewer": "source-viewer", "title_input": "source-viewer input.title-input", "close_view": [ "source-viewer button[mattooltip='Close source view']", "button[mattooltip='Close source view']", ], "open_in_new": "source-viewer button[aria-label='Open in new tab']", "source_guide_toggle": "source-viewer button[aria-label='Close source guide']", "guide_keyword_chip": "source-viewer button[role='option']", } ``` Live source-add submit check (2026-06-15): copied text, Website URL (`https://example.com`), and file upload all closed the modal and increased the source-card count. Drive opens a Google picker iframe; selecting a Drive file was not needed to verify the entry point. Live source-viewer check (2026-06-15): opening a source showed `source-viewer`, `input.title-input`, `Open in new tab`, a `Close source guide` toggle, source-guide keyword chips with `role='option'`, and a tooltiped close button (`mattooltip='Close source view'`). ### RPC: ADD_SOURCE (izAoDd) - URL **Source:** `_source/add.py::SourceAddService.add_url_source()` ```python # URL goes at position [2] in an 11-element source spec. params = [ [[None, None, [url], None, None, None, None, None, None, None, 1]], notebook_id, # 1: Notebook ID [2, None, None, [1, None, None, None, None, None, None, None, None, None, [1]]], # 2: Shared request-options wrapper ] ``` ### RPC: ADD_SOURCE (izAoDd) - Text **Source:** `_source/add.py::SourceAddService.add_text()` ```python # [title, content] at position [1] in an 11-element source spec; slot [3] is # the captured source-type code for pasted text. params = [ [[None, [title, content], None, 2, None, None, None, None, None, None, 1]], notebook_id, [2, None, None, [1, None, None, None, None, None, None, None, None, None, [1]]], ] ``` ### RPC: ADD_SOURCE (izAoDd) - YouTube **Source:** `_source/add.py::SourceAddService.add_youtube_source()` ```python # YouTube URL at position [7] in the source spec (different from regular URL). params = [ [[None, None, None, None, None, None, None, [url], None, None, 1]], # 0 notebook_id, # 1 [2, None, None, [1, None, None, None, None, None, None, None, None, None, [1]]], # 2: Shared wrapper ] ``` ### RPC: ADD_SOURCE (izAoDd) - Google Drive **Source:** `_source/add.py::SourceAddService.add_drive()` ```python # Drive source structure - single-wrapped (not double!) source_data = [ [file_id, mime_type, 1, title], # 0: File info None, None, None, None, None, # 1-5: Padding None, None, None, None, # 6-9: Padding 1, # 10: Trailing flag ] params = [ [source_data], # 0: Single-wrapped (NOT [[source_data]]) notebook_id, # 1: Notebook ID [2], # 2: Source type flag [1, None, None, None, None, None, None, None, None, None, [1]], # 3: Config ] ``` **Note:** Drive add is intentionally still on the older `[2]`, `[1, ..., [1]]` tail pending a fresh live Drive capture. URL, YouTube, text, CREATE_NOTEBOOK, and ADD_SOURCE_FILE use the shared nested wrapper from `_source/upload_payloads.py::build_template_block()`. ### RPC: ADD_SOURCE_FILE (o4cbdc) - File Upload Registration **Source:** `_source/upload.py::SourceUploadPipeline.register_file_source()`, `_source/upload_payloads.py::build_register_file_source_params()`, `_source/upload_payloads.py::build_resumable_upload_start_request()` File uploads are a two-step flow. First, `ADD_SOURCE_FILE` registers the file source and returns a `SOURCE_ID`; then the client starts a Scotty resumable upload session and streams the bytes to the `x-goog-upload-url` returned by that start request. ```python params = [ [[filename]], # 0: Filename wrapped twice notebook_id, # 1: Notebook ID [2, None, None, [1, None, None, None, None, None, None, None, None, None, [1]]], # 2: Shared request-options wrapper ] # Called with source_path: await rpc_call( RPCMethod.ADD_SOURCE_FILE, params, source_path=f"/notebook/{notebook_id}", allow_null=False, disable_internal_retries=True, ) ``` Registration is mutating, so the upload pipeline uses the same probe-then-create idempotency pattern as URL and Drive sources. Because filenames are not unique, the probe records source IDs before the create and only trusts a same-title source if it is new since that baseline. ```python # Start resumable upload after SOURCE_ID registration: start_headers = { "Accept": "*/*", "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", "Origin": base_url, "Referer": f"{base_url}/", "x-goog-authuser": authuser, "x-goog-upload-command": "start", "x-goog-upload-header-content-length": str(file_size), "x-goog-upload-header-content-type": content_type, "x-goog-upload-protocol": "resumable", } start_body = { "PROJECT_ID": notebook_id, "SOURCE_NAME": filename, "SOURCE_ID": source_id, } # Stream bytes to response.headers["x-goog-upload-url"]: upload_headers = { "Accept": "*/*", "Content-Type": "application/x-www-form-urlencoded;charset=utf-8", "x-goog-authuser": authuser, "Origin": base_url, "Referer": f"{base_url}/", "x-goog-upload-command": "upload, finalize", "x-goog-upload-offset": "0", } ``` ### RPC: DELETE_SOURCE (tGMBJ) **Source:** `_sources.py::delete()` **IMPORTANT:** `notebook_id` is passed via `source_path`, NOT in params! ```python params = [[[source_id]]] # Triple-nested! # Called with: await rpc_call( RPCMethod.DELETE_SOURCE, params, source_path=f"/notebook/{notebook_id}", # <-- notebook_id here ) ``` ### RPC: UPDATE_SOURCE / Rename (b7Wfje) **Source:** `_sources.py::rename()` ```python # Different structure: None at [0], source_id at [1], title triple-nested at [2] params = [ None, # 0 [source_id], # 1: Single-nested source ID [[[new_title]]], # 2: Triple-nested title ] ``` ### RPC: GET_SOURCE_GUIDE (tr032e) **Source:** `_sources.py::get_guide()` ```python # Quadruple-nested source ID! params = [[[[source_id]]]] ``` ### RPC: GET_SOURCE (hizoJc) **Source:** `_source/content.py::get_fulltext()` **Purpose:** Get raw text or clean HTML/markdown content of a source. **Params:** ```python # Position 0: Single-nested source ID # Position 1: Output type: [2] for plain text, [3] for cleaned HTML/markdown structure # Position 2: Format selector matching position 1 params = [ [source_id], # 0 [2], # 1 [2], # 2 ] # Markdown/HTML source rendering uses the same selector in both slots: params = [ [source_id], [3], [3], ] ``` **Request format:** ```python await rpc_call( RPCMethod.GET_SOURCE, params, source_path=f"/notebook/{notebook_id}", ) ``` --- ## Source Labels Source labels group a notebook's sources into AI-generated (or manually named) topic buckets. A label is a standalone entity — a source carries no back-reference; the label owns a list of source IDs, and membership is many-to-many (a source can belong to multiple labels). Every label RPC's first argument is the recurring request-options wrapper used by `_settings.py`: ```python OPTS = [2, None, None, [1, None, None, None, None, None, None, None, None, None, [1]]] ``` ### The Label Tuple (response shape) Each label is a 4-tuple `[name, sources, label_id, emoji]`: | Slot | Field | Notes | |------|-------|-------| | `[0]` | `name` | str | | `[1]` | `sources` | `[[source_id], ...]` (each UUID wrapped in its own 1-element list); **`None`** for a new empty label | | `[2]` | `label_id` | server-assigned UUID | | `[3]` | `emoji` | `""` when unset, else the emoji string | **Response envelopes differ by RPC:** `CREATE_LABEL` returns `[None, [label, ...]]` (label set at index `[1]`); `LIST_LABELS` returns `[[label, ...]]` (label set at index `[0]`); `UPDATE_LABEL` and `DELETE_LABEL` echo `[]` on success. ### UI Selectors Live-checked on 2026-06-15 after NotebookLM generated source labels for the probe notebook. ```python LABEL_UI_SELECTORS = { # Before labels exist, the same button is labelled "Auto-label your sources # by topic"; after labels exist, it becomes "Undo or re-label sources". "auto_label_menu": [ "button[aria-label='Auto-label your sources by topic']", "button[aria-label='Undo or re-label sources']", ], "add_new_label": "button[role='menuitem']:has-text('Add new label')", "reorganize": "button[role='menuitem']:has-text('Reorganize')", "reorganize_all_sources": "button[role='menuitem']:has-text('All sources')", "return_to_list_view": "button[role='menuitem']:has-text('Return to list view')", "label_panel": "source-picker mat-expansion-panel", "label_header": "source-picker mat-expansion-panel-header", "label_name": "source-picker .label-name", "label_checkbox": "source-picker input[aria-label='