Skip to content

feat(zoom): discover sessions by host allowlist and Zoom Group - #14517

Open
Subash-Mohan wants to merge 2 commits into
Subash-Mohan/zoom-webinar-supportfrom
Subash-Mohan/zoom-host-group-discovery
Open

feat(zoom): discover sessions by host allowlist and Zoom Group#14517
Subash-Mohan wants to merge 2 commits into
Subash-Mohan/zoom-webinar-supportfrom
Subash-Mohan/zoom-host-group-discovery

Conversation

@Subash-Mohan

@Subash-Mohan Subash-Mohan commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Description

  • Adds two optional Discovery mechanisms alongside the existing ID allowlist: a host-email allowlist and a Zoom Group resolved to its members. Whatever is configured is unioned, and setup is still rejected when all three are empty.
  • These two mechanisms reach Sessions older than 15 months, which the Meeting ID path cannot — GET /users/{userId}/recordings declares no age limit, so depth is bounded by the account's own recording auto-delete policy instead.
  • A host email that matches no active Zoom user is reported as a failure rather than silently indexing nothing, so a typo in the config is visible to the admin.
  • Recordings that are not a Meeting or Webinar — a file uploaded through Zoom's web Recordings page, or a type code Zoom adds in future — are skipped rather than indexed under a guessed Session type.

How Has This Been Tested?

Unit tests added (258 in the Zoom suite, 1493 across all connectors, all passing). The daily suite gained host-allowlist and Group cases against the real API, which need two new secrets — zoom-test-host-email and zoom-test-group-id — and have not been run against a live account yet.

Additional Options

  • [Optional] Please cherry-pick this PR to the latest release version.
  • [Optional] Override Linear Check

Summary by cubic

Adds two optional Zoom discovery mechanisms, a host-email allowlist and a Zoom Group, that union with the existing ID allowlist and reach sessions older than its 15-month cap. Setup is still rejected when all three are empty.

New Features

  • Host emails are resolved against the account's user listing; an email that matches no active Zoom user is reported as a failure so a typo is visible to the admin.
  • Recordings that are not a Meeting or Webinar (web-portal uploads and unknown type codes) are skipped rather than indexed under a guessed session type.
  • Recording pages hold 30 items because Zoom expires a page token after 15 minutes; a crawl resuming with an expired token restarts that host from its first page, so its remaining recordings are found instead of permanently lost, and only a second consecutive failure is reported.
  • The daily suite gains host-allowlist and Group cases that need two new secrets, zoom-test-host-email and zoom-test-group-id, which haven't been run against a live account yet.

Written for commit 63d17b6. Summary will update on new commits.

Review in cubic

Both mechanisms walk GET /users/{userId}/recordings, which declares no age
limit, so they reach history older than the meeting-ID path's 15-month cap.
Entries carry the occurrence uuid, topic and start time, so processing needs
no extra call.

Host emails resolve through GET /users rather than trusting the undocumented
convention that {userId} accepts an email; an email matching nobody is
reported instead of quietly indexing nothing. Pages hold 30 recordings because
Zoom expires a next_page_token after 15 minutes and every occurrence on a page
is processed before the next is requested.

A recording's type code decides the session type. Anything outside Zoom's
closed enum is skipped rather than assumed to be a meeting, since the document
id freezes that choice and the access-list endpoint will be picked from it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBNe54dXPELTXXGdk1uYR6
@Subash-Mohan
Subash-Mohan requested a review from a team as a code owner September 5, 2026 09:55
@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds Zoom recording discovery through host-email allowlists and Zoom Groups, unions these sources with ID allowlists, and classifies recording entries as meetings, webinars, or unsupported uploads.

  • Adds paginated Zoom clients for users, group members, and user recordings.
  • Adds checkpointed host traversal and recording pagination.
  • Adds recording-type mapping and broad unit and daily-test coverage.
  • The new checkpoint stores two unstable external positions: an expiring page token and an index into a re-resolved host list.

Confidence Score: 3/5

The PR is not safe to merge until Zoom pagination and host traversal can resume without expired tokens or shifted host positions.

Interrupted attempts can restore an expired Zoom page token, and re-resolving a changed host set can reinterpret a positional cursor. Either path can omit recordings from the completed crawl.

Files Needing Attention: backend/onyx/connectors/zoom/recordings/discovery.py

Important Files Changed

Filename Overview
backend/onyx/connectors/zoom/client.py Adds encoded, paginated clients for account users, group members, and user recordings.
backend/onyx/connectors/zoom/models.py Adds typed models for Zoom users, recording entries, and paginated responses.
backend/onyx/connectors/zoom/recordings/discovery.py Adds host and group discovery, but its durable checkpoint relies on an expiring token and mutable positional host ordering.
backend/onyx/connectors/zoom/recordings/session_types.py Maps known recording type codes to meeting or webinar types and rejects unsupported entries.
backend/onyx/connectors/zoom/connector.py Wires the new discovery sources into connector construction and the existing checkpoint pipeline.
backend/tests/unit/onyx/connectors/zoom/test_zoom_discovery.py Covers source resolution, pagination, type handling, and failures, but not delayed token resumption or membership changes between attempts.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Resolve configured hosts] --> B[Sort hosts by user ID]
  B --> C[Select host by host_index]
  C --> D[List recording page]
  D --> E[Process recording entries]
  E --> F{More recording pages?}
  F -->|Yes| G[Checkpoint host_index and Zoom page token]
  F -->|No| H[Checkpoint next host_index]
  G --> I[Interrupted attempt]
  H --> I
  I --> J[Fresh connector resolves hosts again]
  J --> K[Restore positional index and expiring token]
  K --> L[Token may be expired or host index may have shifted]
Loading
Prompt To Fix All With AI
### Issue 1
backend/onyx/connectors/zoom/recordings/discovery.py:388-395
**Checkpoint Stores Expiring Token**

An interrupted crawl saves Zoom's `next_page_token`, even though Zoom expires it after 15 minutes. A later attempt can send the expired token. A non-retryable response then advances past the host's remaining recordings, while a retryable response can keep failing from the same checkpoint. This can leave recordings unindexed or prevent the crawl from completing.

### Issue 2
backend/onyx/connectors/zoom/recordings/discovery.py:330-353
**Host Cursor Can Shift**

The checkpoint stores only a host's position in a newly resolved and sorted member list. If group membership or active-user resolution changes before an interrupted attempt resumes, an insertion or removal can make that position refer to another host. The crawl can then repeat one host and skip another, leaving the skipped host's recordings unindexed.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "feat(zoom): discover sessions by host al..." | Re-trigger Greptile

Comment on lines +388 to +395
if next_page_token:
return DiscoveryStepResult(
work=work,
failures=failures,
next_cursor={
"host_index": position.host_index,
"page_token": next_page_token,
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Checkpoint Stores Expiring Token

An interrupted crawl saves Zoom's next_page_token, even though Zoom expires it after 15 minutes. A later attempt can send the expired token. A non-retryable response then advances past the host's remaining recordings, while a retryable response can keep failing from the same checkpoint. This can leave recordings unindexed or prevent the crawl from completing.

Knowledge Base Used: Connector ingestion and indexing

Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/onyx/connectors/zoom/recordings/discovery.py
Line: 388-395

Comment:
**Checkpoint Stores Expiring Token**

An interrupted crawl saves Zoom's `next_page_token`, even though Zoom expires it after 15 minutes. A later attempt can send the expired token. A non-retryable response then advances past the host's remaining recordings, while a retryable response can keep failing from the same checkpoint. This can leave recordings unindexed or prevent the crawl from completing.

**Knowledge Base Used:** [Connector ingestion and indexing](https://app.greptile.com/onyx/-/custom-context/knowledge-base/onyx-dot-app/onyx/-/docs/connector-ingestion-and-indexing.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +330 to +353
# Zoom promises no order. A resumed run resolves again, and the cursor
# is an index into this list, so a different order would step past a
# host that was never crawled.
hosts.sort(key=lambda host: host.user_id)
self._resolved = hosts
return hosts, failures

def discover_step(
self,
client: ZoomClient,
start: SecondsSinceUnixEpoch,
end: SecondsSinceUnixEpoch,
cursor: dict[str, Any] | None,
) -> DiscoveryStepResult:
position = (
_UserRecordingsCursor.model_validate(cursor)
if cursor
else _UserRecordingsCursor()
)
hosts, failures = self._hosts(client, start, end)
if position.host_index >= len(hosts):
return DiscoveryStepResult(failures=failures, done=True)

host = hosts[position.host_index]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Host Cursor Can Shift

The checkpoint stores only a host's position in a newly resolved and sorted member list. If group membership or active-user resolution changes before an interrupted attempt resumes, an insertion or removal can make that position refer to another host. The crawl can then repeat one host and skip another, leaving the skipped host's recordings unindexed.

Knowledge Base Used: Connector ingestion and indexing

Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/onyx/connectors/zoom/recordings/discovery.py
Line: 330-353

Comment:
**Host Cursor Can Shift**

The checkpoint stores only a host's position in a newly resolved and sorted member list. If group membership or active-user resolution changes before an interrupted attempt resumes, an insertion or removal can make that position refer to another host. The crawl can then repeat one host and skip another, leaving the skipped host's recordings unindexed.

**Knowledge Base Used:** [Connector ingestion and indexing](https://app.greptile.com/onyx/-/custom-context/knowledge-base/onyx-dot-app/onyx/-/docs/connector-ingestion-and-indexing.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Full-stack Preview (frontend + backend)

Status Preview Commit Updated
https://63d17b6-onyx.preview.onyxcorp.dev/ 63d17b6 2026-09-05 10:07:14 UTC

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 11 files

Confidence score: 2/5

  • backend/onyx/connectors/zoom/client.py: Full-history loads can send a 1970-to-present recording window that Zoom rejects, preventing historical recording discovery; split the request into allowed one-month windows before calling Zoom.
  • backend/onyx/connectors/zoom/recordings/discovery.py: Checkpoint retries can apply host_index to a changed host list and skip hosts after membership or active-status changes; persist a stable user ID in the cursor and resume from that host.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/onyx/connectors/zoom/recordings/discovery.py">

<violation number="1" location="backend/onyx/connectors/zoom/recordings/discovery.py:353">
P2: When group membership or active host status changes before a checkpoint retry, `host_index` is applied to a different host list and can skip unprocessed hosts. Persist a stable user ID in the cursor and resume by that ID instead of relying on the list position.</violation>
</file>

<file name="backend/onyx/connectors/zoom/client.py">

<violation number="1" location="backend/onyx/connectors/zoom/client.py:358">
P1: On a full-history load, `_poll_window_dates(0, now)` produces a range from 1970 to today, but Zoom rejects recording requests spanning more than one month. Split the history into allowed one-month windows before calling this endpoint, otherwise host and Group discovery fails before returning any recordings.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

doesn't exist, so swallowing it would turn a mistyped host email into an
empty index with nothing to explain it."""
params: dict[str, Any] = {
"from": from_date.isoformat(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: On a full-history load, _poll_window_dates(0, now) produces a range from 1970 to today, but Zoom rejects recording requests spanning more than one month. Split the history into allowed one-month windows before calling this endpoint, otherwise host and Group discovery fails before returning any recordings.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/onyx/connectors/zoom/client.py, line 358:

<comment>On a full-history load, `_poll_window_dates(0, now)` produces a range from 1970 to today, but Zoom rejects recording requests spanning more than one month. Split the history into allowed one-month windows before calling this endpoint, otherwise host and Group discovery fails before returning any recordings.</comment>

<file context>
@@ -292,6 +309,69 @@ def list_past_webinar_occurrences(
+        doesn't exist, so swallowing it would turn a mistyped host email into an
+        empty index with nothing to explain it."""
+        params: dict[str, Any] = {
+            "from": from_date.isoformat(),
+            "to": to_date.isoformat(),
+            "page_size": page_size,
</file context>

if position.host_index >= len(hosts):
return DiscoveryStepResult(failures=failures, done=True)

host = hosts[position.host_index]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When group membership or active host status changes before a checkpoint retry, host_index is applied to a different host list and can skip unprocessed hosts. Persist a stable user ID in the cursor and resume by that ID instead of relying on the list position.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/onyx/connectors/zoom/recordings/discovery.py, line 353:

<comment>When group membership or active host status changes before a checkpoint retry, `host_index` is applied to a different host list and can skip unprocessed hosts. Persist a stable user ID in the cursor and resume by that ID instead of relying on the list position.</comment>

<file context>
@@ -188,10 +229,273 @@ def discover_step(
+        if position.host_index >= len(hosts):
+            return DiscoveryStepResult(failures=failures, done=True)
+
+        host = hosts[position.host_index]
+        from_date, to_date = _poll_window_dates(start, end)
+
</file context>

Comment thread backend/onyx/connectors/zoom/recordings/discovery.py
Zoom expires a next_page_token 15 minutes after issuing it, so a crawl
interrupted mid-host resumes holding a dead one. Reporting that abandoned
every recording after that page for good, not just for this run: an entity
failure ends the attempt COMPLETED_WITH_ERRORS, which Onyx counts as a
success, so the next run moves its poll window on and never returns.

Discovery now restarts that host from its first page instead. The duplicate
fetches are absorbed by the upsert, and the restart is tried once in a row, so
a page that reliably outlives the expiry reports and moves on rather than
looping.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBNe54dXPELTXXGdk1uYR6

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 existing issue remains and 1 new issue found across 2 files (changes from recent commits).

Confidence score: 2/5

  • In backend/onyx/connectors/zoom/recordings/discovery.py, continuation-page errors other than expired-token failures can restart the host; because the following cursor omits restarted, a persistent page error may repeatedly restart or fail to make progress. Restrict restart handling to token expiry and preserve the restart state across the next cursor.
  • In backend/onyx/connectors/zoom/recordings/discovery.py, resuming from only position.host_index can target the wrong host when group members are re-resolved and reordered, causing a resumed crawl to skip a host. Persist the host identity or a stable host snapshot in the restart cursor.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/onyx/connectors/zoom/recordings/discovery.py">

<violation number="1" location="backend/onyx/connectors/zoom/recordings/discovery.py:387">
P1: When any non-run-fatal error occurs on a continuation page, this branch restarts the host, not just expired-token failures. After the restarted first page succeeds, the next cursor omits `restarted`. A persistent page-2 error therefore restarts forever without reporting a failure or advancing. Restrict this retry to a verified expired-token response or carry a single retry budget through the restarted walk.</violation>
</file>

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.

Re-trigger cubic

# so the next run moves its poll window on and never comes back.
# Walking the host again from its first page costs duplicates the
# upsert absorbs, and is only tried once in a row.
if position.page_token and not position.restarted:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When any non-run-fatal error occurs on a continuation page, this branch restarts the host, not just expired-token failures. After the restarted first page succeeds, the next cursor omits restarted. A persistent page-2 error therefore restarts forever without reporting a failure or advancing. Restrict this retry to a verified expired-token response or carry a single retry budget through the restarted walk.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/onyx/connectors/zoom/recordings/discovery.py, line 387:

<comment>When any non-run-fatal error occurs on a continuation page, this branch restarts the host, not just expired-token failures. After the restarted first page succeeds, the next cursor omits `restarted`. A persistent page-2 error therefore restarts forever without reporting a failure or advancing. Restrict this retry to a verified expired-token response or carry a single retry budget through the restarted walk.</comment>

<file context>
@@ -374,6 +378,27 @@ def discover_step(
+            # so the next run moves its poll window on and never comes back.
+            # Walking the host again from its first page costs duplicates the
+            # upsert absorbs, and is only tried once in a row.
+            if position.page_token and not position.restarted:
+                logger.warning(
+                    "Restarting Zoom recordings for %s from the first page: %s",
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant