# YouTube Related Videos Graph Crawler (`sian.agency/youtube-related-videos-crawler`) Actor

Crawl YouTube's related-videos graph multi-hop deep from any seed video. BFS traversal with configurable depth + branch + node cap. Each row carries depth, parent video, and full discovery path. Built for content strategy, brand safety, and recommendation-algorithm research.

- **URL**: https://apify.com/sian.agency/youtube-related-videos-crawler.md
- **Developed by:** [SIÁN OÜ](https://apify.com/sian.agency) (community)
- **Categories:** Videos, Social media, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.50 / 1,000 related video rows

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).


# README

## YouTube Related Videos Graph Crawler — Multi-Hop BFS 🔁🚀

[![Store-SIÁN Agency](https://img.shields.io/badge/Store-SI%C3%81N%20Agency-1AE392)](https://apify.com/sian.agency?fpr=sian) [![Store-YouTube Auto Complete](https://img.shields.io/badge/Store-YouTube%20Auto%20Complete-FF0000)](https://apify.com/sian.agency/youtube-auto-complete-and-query-suggestion?fpr=sian) [![Store-YouTube Comments Scraper](https://img.shields.io/badge/Store-YouTube%20Comments%20Scraper-FF0000)](https://apify.com/sian.agency/cheapest-youtube-comments-scraper?fpr=sian) [![Store-YouTube AI Comments](https://img.shields.io/badge/Store-YouTube%20AI%20Comments-FF0000)](https://apify.com/sian.agency/youtube-ai-comments-scraper-and-questions-extractor?fpr=sian)

#### 🎉 The ONLY actor that crawls YouTube's related-videos algorithm multi-hop deep — up to 4 hops
##### Perfect for content-strategy teams, brand-safety auditors, YouTube SEO researchers, ML training-set builders & recommendation-algorithm studies.

---

### 📋 Overview

**Map the topical neighborhood YouTube's algorithm associates with any video.** Every other related-video scraper hands you a flat list from one API call — this one walks the graph BFS, up to 4 hops out, with global dedup and configurable branch factor. Each row carries `depth`, `discoveredVia` (parent videoId), and `discoveryPath` (full hop chain back to seed) — ready for NetworkX, Gephi, BigQuery, or any graph visualization tool.

**Why content strategists, ML teams & researchers choose us:**
- ✅ **Only multi-hop graph crawler on Apify**: confirmed via marketplace scan — no other actor walks YouTube's related-videos graph beyond a single call.
- ⚡ **Bulk seed support**: up to 50 seed videoIds per run with global dedup — overlapping neighborhoods produce a denser cluster, not duplicates.
- 🎯 **Per-row provenance**: every discovered videoId carries its `depth`, `discoveredVia` (parent), and full `discoveryPath` array — build edges in two lines of NetworkX.
- 💰 **BFS efficiency wedge**: $0.005 per unique videoId at BRONZE — at `maxDepth=2`, `branch=10` you map ~100 videos for under $0.55.
- 💎 **Safety-first design**: hard `maxNodes` cap (1–5000) prevents quota burn; geometric explosion stopped mid-crawl when threshold hits.
- ✨ **NEW**: `discoveryPath` array on every row — pivot it in Excel or feed into `nx.add_edges_from(zip(path, path[1:]))` to materialise the full graph instantly.

---

### ✨ Features

- 🌳 **Multi-Hop BFS Traversal**: up to 4 hops deep from any seed videoId — the entire topical neighborhood in one run.
- 📚 **Bulk Seed Mode**: up to 50 seeds per run, with global dedup across all branches.
- 🛤 **Per-Row Provenance**: `depth`, `discoveredVia` (parent), `discoveryPath` (full hop chain) — graph-ready out of the box.
- 🎛 **Configurable Branch Factor**: `maxBranchPerNode` from 1 (chain traversal) to 20 (wide breadth).
- 🛡️ **Safety Cap**: `maxNodes` (1–5000) stops runaway crawls before they burn quota.
- 🎥 **Shorts & Playlist Passthrough**: optional toggles to emit `shorts_listing` and `playlist` items as leaf rows.
- 📊 **Parsed View Counts**: `viewCount` integer + `viewCountText` raw — no more parsing "9.6M views" yourself.
- 📦 **Schema-Stable Output**: every row matches a published dataset schema — predictable for downstream ETL.
- 🛡️ **No Account, No API Key, No Proxies**: just an Apify token and you're crawling.

---

### 🎬 Quick Start

Pass a seed videoId, pick a depth, and run. Each unique discovered videoId becomes one dataset row, stamped with depth and discovery path.

```bash
curl -X POST "https://api.apify.com/v2/acts/sian.agency~youtube-related-videos-crawler/runs?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"videoId":"dQw4w9WgXcQ","maxDepth":2,"maxBranchPerNode":10,"maxNodes":120}'
````

***

### 🚀 Getting Started (3 Simple Steps)

#### Step 1: Pick your seed(s)

- Single-seed mode: paste a video ID or any YouTube URL into `videoId`.
- Bulk-seed mode: paste up to 50 video IDs (one per line) into `seedVideoIds`.

#### Step 2: Tune the crawl

- `maxDepth` (0–4) — how many hops out (0 = seed only, 1 = direct neighbors, 2 = ~100 videos, 3 = ~500).
- `maxBranchPerNode` (1–20) — width per node. 10 is the balanced default.
- `maxNodes` (1–5000) — hard cap on total unique videos. 500 is the safe default.

#### Step 3: Run it

Hit run. Watch the dataset fill with one row per unique discovered videoId. Filter by `depth` if you only want close neighbors.

**That's it! In seconds, you'll have:**

- A graph-ready dataset with `depth`, `discoveredVia`, and `discoveryPath` per row
- Parsed view counts, channel info, thumbnails, and publish dates
- HTML report with depth breakdown, success rate, and upstream call count

***

### 📥 Input Configuration

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `videoId` | string | One of | Single seed video ID or YouTube URL |
| `seedVideoIds` | string | One of | Bulk seeds, one per line or comma-separated. Max 50 |
| `maxDepth` | integer | No | Hops deep (0–4). Default `1`. 0 = seed only |
| `maxBranchPerNode` | integer | No | Related videos to follow per node (1–20). Default `10` |
| `maxNodes` | integer | No | Hard cap on total unique videos (1–5000). Default `500` |
| `maxPagesPerNode` | integer | No | Continuation pages per node (1–5). Default `1` |
| `includeShorts` | boolean | No | Emit `shorts_listing` items as leaf rows. Default `true` |
| `includePlaylists` | boolean | No | Emit `playlist` items as leaf rows. Default `false` |
| `geo` | string | No | ISO 3166-1 alpha-2 country code. Defaults to `US` |
| `lang` | string | No | ISO 639-1 language code. Defaults to `en` |

**Sizing intuition:** rows ≈ `1 + branch + branch² + ... + branch^maxDepth`, capped by `maxNodes`.

| Preset | maxDepth | maxBranchPerNode | maxNodes | Expected rows |
|---|---|---|---|---|
| Quick neighborhood | 1 | 10 | 500 | ~10 |
| Topical cluster | 2 | 10 | 200 | ~100 |
| Deep graph | 3 | 10 | 500 | ~500 |
| Algorithm trace | 4 | 3 | 200 | ~120 |
| Wide bulk study (5 seeds) | 2 | 5 | 500 | ~155 |

**Example — topical-cluster study (~100 rows):**

```json
{
  "videoId": "dQw4w9WgXcQ",
  "maxDepth": 2,
  "maxBranchPerNode": 10,
  "maxNodes": 120
}
```

**Example — bulk-seed graph fusion:**

```json
{
  "seedVideoIds": "dQw4w9WgXcQ\n9bZkp7q19f0\nkJQP7kiw5Fk",
  "maxDepth": 2,
  "maxBranchPerNode": 5,
  "maxNodes": 400
}
```

***

### 📤 Output

Results are saved to the Apify dataset with **20+ fields** including:

| Field | Type | Description |
|-------|------|-------------|
| `depth` | integer | Hops from seed. 0 = seed itself |
| `discoveredVia` | string | Parent videoId from which this video was reached |
| `discoveryPath` | array | Ordered videoIds from seed → this row |
| `itemType` | string | `video`, `shorts`, `playlist`, or `seed` |
| `videoId` | string | 11-character YouTube video ID |
| `videoPageUrl` | string | Canonical watch URL |
| `videoTitle` | string | Title of the video / Short / playlist |
| `channelId` | string | Canonical channel ID (`UC…`) |
| `channelTitle` | string | Channel display name |
| `channelHandle` | string | Channel handle (e.g. `@RickAstleyYT`) when available |
| `channelPageUrl` | string | Canonical channel page URL |
| `viewCount` | integer | Parsed view count (e.g. 9600000 from "9.6M views") |
| `viewCountText` | string | Raw view-count text from upstream |
| `lengthText` | string | Duration string (e.g. `3:32`) |
| `publishedAt` | string | ISO 8601 published timestamp |
| `thumbnailUrl` | string | Highest-resolution thumbnail URL |
| `status` | string | `success`, `error`, or `no_related_videos` |

**Example row (depth=1 discovered video):**

```json
{
  "_fetchedAt": "2026-05-22T08:42:11.013Z",
  "_seedVideoId": "dQw4w9WgXcQ",
  "status": "success",
  "itemType": "video",
  "depth": 1,
  "discoveredVia": "dQw4w9WgXcQ",
  "discoveryPath": ["dQw4w9WgXcQ", "yPYZpwSpKmA"],
  "videoId": "yPYZpwSpKmA",
  "videoPageUrl": "https://www.youtube.com/watch?v=yPYZpwSpKmA",
  "videoTitle": "Rick Astley - Together Forever (Official Video) [4K Remaster]",
  "channelId": "UCuAXFkgsw1L7xaCfnd5JJOw",
  "channelTitle": "Rick Astley",
  "channelHandle": "@RickAstleyYT",
  "viewCount": 28000000,
  "viewCountText": "28M views",
  "lengthText": "3:32",
  "publishedAt": "2025-05-21T00:00:00Z",
  "thumbnailUrl": "https://i.ytimg.com/vi/yPYZpwSpKmA/hqdefault.jpg"
}
```

The seed itself is emitted as `depth=0` with `itemType: "seed"` (not charged). Errors come back as rows with `status: "error"`. Empty seeds get `status: "no_related_videos"` (no charge).

***

### 💼 Use Cases & Examples

#### 1. Content Strategy — Topical Neighborhood Mapping

**Content-strategy teams use this to map adjacent content ideas before they trend.**

**Input:** A viral hit videoId at `maxDepth=3, maxBranchPerNode=10`.
**Output:** ~500 related videos with channel, view counts, and discovery path.
**Use:** Identify the topical cluster YouTube's algorithm associates with your seed — plan content series around it.

#### 2. Brand Safety — Adjacency Audit

**Brand-safety teams use this to flag off-brand or unsafe content sitting next to their ads.**

**Input:** Each of your brand's YouTube ad videoIds, crawled at `maxDepth=2`.
**Output:** Up to ~110 neighboring videos per ad with channel + title metadata.
**Use:** Audit what content sits in the recommendation neighborhood — flag risky adjacencies in minutes.

#### 3. YouTube SEO — Reverse-Engineer Algorithm Signals

**YouTube SEO consultants use this to align their titles, tags, and descriptions with what the algorithm associates.**

**Input:** Your channel's top-performing videoIds as seeds.
**Output:** What the algorithm thinks each video relates to, across hops.
**Use:** Optimize metadata to land in the discovered topical cluster — boost recommendation surfacing.

#### 4. AI / ML — Topically-Clustered Training Sets

**ML researchers use this to produce graph datasets with depth + parent metadata for embedding training.**

**Input:** 50 seed videoIds at `maxDepth=2`, `maxNodes=5000`.
**Output:** A graph dataset of up to 5,000 topically-related videos with edges.
**Use:** Train recommendation models on real YouTube co-occurrence signals — ready for PyTorch Geometric / DGL.

#### 5. Researchers — Recommendation-Algorithm Studies

**Algorithm researchers use this to study echo chambers and graph fanout patterns.**

**Input:** Polarising / political seeds vs entertainment seeds at `maxDepth=3`.
**Output:** Side-by-side neighborhood graphs with full `discoveryPath` arrays.
**Use:** Measure echo-chamber width, compare fanout, export to NetworkX for full network analysis.

#### 6. Competitor Intelligence — Channel Adjacency Mapping

**Competitor-intel teams use this to map which channels live in your category's algorithmic neighborhood.**

**Input:** Top-performing video from each major competitor as seeds.
**Output:** A merged graph showing channel overlap across competitor neighborhoods.
**Use:** Spot which creators dominate the topical cluster — target them for partnerships or competitive analysis.

#### 7. Topic-Cluster SEO — Pillar/Cluster Content Planning

**Content marketers use this to map pillar/cluster topic architectures from a single hero video.**

**Input:** Your pillar video at `maxDepth=2`, `maxBranchPerNode=15`.
**Output:** Hundreds of cluster-topic candidates with engagement metrics.
**Use:** Build a content calendar that maps onto the algorithm's existing topical graph — faster ranking.

***

### 🔗 Integration Examples

#### JavaScript / Node.js

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_TOKEN' });

const run = await client.actor('sian.agency/youtube-related-videos-crawler').call({
  videoId: 'dQw4w9WgXcQ',
  maxDepth: 2,
  maxBranchPerNode: 10,
  maxNodes: 120,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(`Crawled ${items.length} videos across ${Math.max(...items.map(i => i.depth))} hops`);
```

#### Python

```python
from apify_client import ApifyClient
client = ApifyClient('YOUR_TOKEN')

run = client.actor('sian.agency/youtube-related-videos-crawler').call(
    run_input={
        'videoId': 'dQw4w9WgXcQ',
        'maxDepth': 2,
        'maxBranchPerNode': 10,
        'maxNodes': 120,
    }
)

## Build a NetworkX graph from discoveryPath
import networkx as nx
G = nx.DiGraph()
for item in client.dataset(run['defaultDatasetId']).iterate_items():
    path = item.get('discoveryPath') or []
    G.add_edges_from(zip(path, path[1:]))
print(f'Graph: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
```

#### cURL

```bash
curl -X POST "https://api.apify.com/v2/acts/sian.agency~youtube-related-videos-crawler/runs?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"videoId":"dQw4w9WgXcQ","maxDepth":2,"maxBranchPerNode":10,"maxNodes":120}'
```

#### Automation Workflows (N8N / Zapier / Make)

1. **Trigger**: Schedule (e.g. weekly graph refresh) or manual webhook
2. **HTTP Request**: Call this actor's run API with seed videoId(s)
3. **Process**: Iterate dataset rows in your workflow, group by `depth` or `discoveredVia`
4. **Action**: Push to BigQuery / Gephi / Notion / your graph database

***

### 📊 Performance & Pricing

#### FREE Tier (Try It Now)

- Generous free credit covers a small initial neighborhood crawl — full feature access, same data quality.
- No credit card required.
- Perfect for kicking the tires on a single-seed `maxDepth=1` crawl.

#### PAID Tier (Production-Ready)

- **Unlimited** unique videoIds discovered per run.
- Pay-per-event: only billed for discovered videos at depth ≥ 1 — seed echoes, errors, and `no_related_videos` rows are free.
- Pricing ladder scales from BRONZE to DIAMOND as your volume grows.

**Pricing Highlights — `related-video-row` (headline):**

- BRONZE: `$0.005` per unique discovered videoId
- GOLD / PLATINUM / DIAMOND: `$0.0025` per row

**Run start — `apify-actor-start`:**

- BRONZE+ (paying tiers): `$0.002` per run

💰 **Use `maxNodes` as a soft cost cap** — at BRONZE $0.005/row, `maxNodes=200` ⇒ max $1 per run.

🔗 [View live pricing](https://apify.com/sian.agency/youtube-related-videos-crawler?fpr=sian)

***

### ❓ Frequently Asked Questions

**Q: What's the difference between this and a regular "related videos" scraper?**
A: Other actors give you one flat list from a single API call. This one walks the graph — each related video becomes a new starting point for its own related-videos call, up to `maxDepth` hops. You get the entire neighborhood, not just the front door.

**Q: Why BFS and not DFS?**
A: Breadth-first matches "show me the topical neighborhood" intent. The algorithm signals weaken with depth, so BFS surfaces the strongest associations first. For chain-style traversal, set `maxBranchPerNode=1` — BFS with branch=1 IS a depth-first chain.

**Q: Why does my run return fewer rows than `1 + branch + branch²`?**
A: Three reasons: (1) duplicates — videos discovered via multiple paths emit once; (2) the `maxNodes` cap kicks in mid-crawl; (3) some videos return fewer than `maxBranchPerNode` related items.

**Q: Do you crawl Shorts and playlists?**
A: No. Shorts are emitted as leaf rows when `includeShorts=true` but never traversed. Playlists are emitted as leaf rows when `includePlaylists=true` — their cover videoId is captured but not crawled further (playlists are thumbnail anchors, not real graph nodes).

**Q: What happens when a seed is deleted / private / region-blocked?**
A: The seed emits two rows: one `itemType: "seed"` (free) and one `status: "error"` with a friendly error message (free). Other seeds in a bulk run continue normally.

**Q: Can I export to Gephi / NetworkX?**
A: Yes. Every row has `discoveryPath: [seed, hop1, ..., this]`. Build edges from consecutive pairs in each path: `nx.add_edges_from(zip(path, path[1:]))`. The `videoId` is the node ID; `videoTitle` / `channelTitle` are node attributes.

**Q: What output formats are available?**
A: JSON, CSV, Excel, RSS, HTML — export directly from the Apify dataset.

**Q: Is this legal?**
A: Yes — we only extract publicly available data that YouTube already exposes to every viewer. See our legal section below.

*YouTube is a trademark of Google LLC. This actor is an independent tool and is not affiliated with, endorsed by, or sponsored by Google LLC.*

***

### 🐛 Troubleshooting

**"Invalid seed video ID" error**

- YouTube video IDs are exactly 11 characters of alphanumerics, dashes, and underscores. Paste the ID directly or any full YouTube URL — the actor will extract the ID for you.

**Run returns only the seed row**

- Check `maxDepth` — if it's 0, only the seed is emitted by design. Bump to 1 to get direct neighbors.

**Fewer rows than expected**

- Global dedup means videos discovered via multiple paths emit once. Also check whether `maxNodes` is capping the crawl earlier than `maxDepth` would.

**Crawl runs longer than expected**

- Bigger `maxDepth` × `maxBranchPerNode` = geometric growth. Use `maxNodes` to cap total cost and runtime. At `maxDepth=2, branch=10, maxNodes=200` you're typically under a minute.

**Empty `discoveryPath` arrays**

- Only the seed row (`itemType: "seed"`) has a single-element path. All discovered rows have at least `[seed, child]`.

***

### ⚖️ Is it legal to scrape data?

Our actors are ethical and do not extract any private user data, such as email addresses, gender, or location. They only extract what the user has chosen to share publicly. We therefore believe that our actors, when used for ethical purposes by Apify users, are safe.

However, you should be aware that your results could contain personal data. Personal data is protected by the **GDPR** in the European Union and by other regulations around the world. You should not scrape personal data unless you have a legitimate reason to do so. If you're unsure whether your reason is legitimate, consult your lawyers.

You can also read Apify's blog post on the [legality of web scraping](https://blog.apify.com/is-web-scraping-legal/).

***

### 🤝 Support

[![Telegram Support](https://img.shields.io/badge/Telegram-Support%20Group-0088cc?logo=telegram)](https://t.me/+vyh1sRE08sAxMGRi)

**Join our active support community**

- For issues or questions, open an issue in the actor's repository
- Check [SIÁN Agency Store](https://apify.com/sian.agency?fpr=sian) for more automation tools
- ✉️ <apify@sian-agency.online>

***

**Built by [SIÁN Agency](https://www.sian-agency.online)** | **[More Tools](https://apify.com/sian.agency?fpr=sian)**

# Actor input Schema

## `videoId` (type: `string`):

🎬 **Use this for a single-seed crawl.** Leave empty if you want to use the bulk `seedVideoIds` field below.

Accepts:

- 11-character video ID: `dQw4w9WgXcQ`
- Full URL: `https://www.youtube.com/watch?v=dQw4w9WgXcQ`
- Short URL: `https://youtu.be/dQw4w9WgXcQ`
- Shorts URL: `https://www.youtube.com/shorts/X4dGtpUD3gA`

💡 **TIP:** Start with `maxDepth=1` and `maxBranchPerNode=10` for a feel of the neighborhood (~10 rows). Move to `maxDepth=2` (~100 rows) or `maxDepth=3` (~500+ rows) to crawl deeper.

## `seedVideoIds` (type: `string`):

📚 **Use this for a bulk-seed crawl.** Leave empty if you use the single `videoId` field above.

Each seed gets its own BFS graph crawl. Dedup is global across all seeds — a video discovered via Seed A and Seed B is emitted once with its earliest `discoveryPath`.

Accepts video IDs, full watch URLs, short URLs, or Shorts URLs (one per line, or comma-separated). Up to 50 seeds per run.

💡 **TIP:** For a balanced topical-cluster study, pass 5-10 well-chosen seeds at `maxDepth=2`.

## `maxDepth` (type: `integer`):

🔁 **How many hops deep to crawl.** Depth 0 = seed only. Depth 1 = seed + its direct related videos. Depth 2 = seed + neighbors-of-neighbors. Depth 3 = three hops out (~500-1100 rows per seed with branch=10).

Geometric growth: rows ≈ `1 + branch + branch² + ... + branch^maxDepth`. The `maxNodes` cap below stops runaway crawls.

- **Default:** 1 (~10 rows)
- **Min:** 0 (seed only — emits a single row, useful for ID validation)
- **Max:** 4 (use with low branch and tight maxNodes)

## `maxBranchPerNode` (type: `integer`):

🌿 **How many related videos to follow from each crawled node.** Higher = wider local sampling; lower = narrower, deeper chains.

- `1` = follow only the top related video per node — gives chain-like exploration
- `10` = balanced topical neighborhood (default)
- `20` = full breadth (use sparingly — each /related call returns ~20 items)

💡 **TIP:** For a topical-cluster study, branch=10 is the sweet spot. For chain-style algorithm tracing, set branch=3 with depth=4.

## `maxNodes` (type: `integer`):

🛡 **Hard cap on total unique videos discovered across the crawl.** BFS stops the moment this cap is reached, even mid-level.

Protects against geometric explosion: depth=4 × branch=10 = up to 11,111 nodes without a cap. Keeping `maxNodes` realistic prevents quota burn.

- **Default:** 500 (covers most depth=2 crawls + most depth=3 crawls with branch=10)
- **Min:** 1
- **Max:** 5,000

## `maxPagesPerNode` (type: `integer`):

📄 **How many `/related` continuation pages to pull per crawled node** (each page returns ~20 fresh items). Multiplies the local sample at each hop without going deeper.

- **Default:** 1 (single page per node — typical)
- **Min:** 1
- **Max:** 5 (~100 items per node)

💡 **TIP:** Leave at 1 unless you specifically want a fatter local neighborhood from the seed.

## `includeShorts` (type: `boolean`):

🩳 **When the upstream returns a `shorts_listing` carousel** (e.g. "Shorts remixing this video"), unpack and emit each Short as a row.

- `true` = emit Shorts (default)
- `false` = skip Shorts; only emit standard `video` items

Shorts are NOT traversed (no `/related` call is made on them) — they are leaf nodes in the graph. Only the carousel videos themselves are emitted.

## `includePlaylists` (type: `boolean`):

📋 **When the upstream returns `playlist` items** in the related-videos list (e.g. "Mix - Artist Name"), emit them as leaf rows.

- `true` = emit playlist items as rows (the cover videoId is captured)
- `false` = skip playlists entirely (default — playlists are not real related-video nodes; their cover videoId is just a thumbnail anchor)

Playlists are NEVER traversed (no `/related` call is made on the cover videoId).

## `geo` (type: `string`):

🌍 **Optional.** Two-letter country code to localize the recommendation graph. Defaults to `US`.

Examples: `US` (United States), `GB` (United Kingdom), `IN` (India), `BR` (Brazil), `DE` (Germany), `JP` (Japan), `KR` (Korea), `MX` (Mexico).

## `lang` (type: `string`):

🗣 **Optional.** Two-letter language code for localized labels. Defaults to `en`.

Examples: `en`, `es`, `pt`, `hi`, `ja`, `ko`.

## Actor input object example

```json
{
  "videoId": "dQw4w9WgXcQ",
  "seedVideoIds": "dQw4w9WgXcQ\n9bZkp7q19f0\nkJQP7kiw5Fk",
  "maxDepth": 1,
  "maxBranchPerNode": 10,
  "maxNodes": 500,
  "maxPagesPerNode": 1,
  "includeShorts": true,
  "includePlaylists": false,
  "geo": "US",
  "lang": "en"
}
```

# Actor output Schema

## `output` (type: `string`):

Graph rows with curated camelCase fields (videoId, videoTitle, channelTitle, viewCount, depth, discoveredVia, discoveryPath, …). One row per unique videoId discovered in the crawl.

## `report` (type: `string`):

HTML report with run status, success/error counts, depth-by-depth breakdown, upstream calls, duration, and crawl inputs — written even on fatal crash.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "videoId": "dQw4w9WgXcQ"
};

// Run the Actor and wait for it to finish
const run = await client.actor("sian.agency/youtube-related-videos-crawler").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "videoId": "dQw4w9WgXcQ" }

# Run the Actor and wait for it to finish
run = client.actor("sian.agency/youtube-related-videos-crawler").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "videoId": "dQw4w9WgXcQ"
}' |
apify call sian.agency/youtube-related-videos-crawler --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=sian.agency/youtube-related-videos-crawler",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "YouTube Related Videos Graph Crawler",
        "description": "Crawl YouTube's related-videos graph multi-hop deep from any seed video. BFS traversal with configurable depth + branch + node cap. Each row carries depth, parent video, and full discovery path. Built for content strategy, brand safety, and recommendation-algorithm research.",
        "version": "1.0",
        "x-build-id": "aBwkzqX67tU4u4xxf"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/sian.agency~youtube-related-videos-crawler/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-sian.agency-youtube-related-videos-crawler",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/sian.agency~youtube-related-videos-crawler/runs": {
            "post": {
                "operationId": "runs-sync-sian.agency-youtube-related-videos-crawler",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/sian.agency~youtube-related-videos-crawler/run-sync": {
            "post": {
                "operationId": "run-sync-sian.agency-youtube-related-videos-crawler",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "properties": {
                    "videoId": {
                        "title": "🎬 Seed video ID or YouTube URL (single-seed mode)",
                        "type": "string",
                        "description": "🎬 **Use this for a single-seed crawl.** Leave empty if you want to use the bulk `seedVideoIds` field below.\n\nAccepts:\n- 11-character video ID: `dQw4w9WgXcQ`\n- Full URL: `https://www.youtube.com/watch?v=dQw4w9WgXcQ`\n- Short URL: `https://youtu.be/dQw4w9WgXcQ`\n- Shorts URL: `https://www.youtube.com/shorts/X4dGtpUD3gA`\n\n💡 **TIP:** Start with `maxDepth=1` and `maxBranchPerNode=10` for a feel of the neighborhood (~10 rows). Move to `maxDepth=2` (~100 rows) or `maxDepth=3` (~500+ rows) to crawl deeper."
                    },
                    "seedVideoIds": {
                        "title": "📚 Bulk seed video IDs (one per line, or comma-separated)",
                        "type": "string",
                        "description": "📚 **Use this for a bulk-seed crawl.** Leave empty if you use the single `videoId` field above.\n\nEach seed gets its own BFS graph crawl. Dedup is global across all seeds — a video discovered via Seed A and Seed B is emitted once with its earliest `discoveryPath`.\n\nAccepts video IDs, full watch URLs, short URLs, or Shorts URLs (one per line, or comma-separated). Up to 50 seeds per run.\n\n💡 **TIP:** For a balanced topical-cluster study, pass 5-10 well-chosen seeds at `maxDepth=2`."
                    },
                    "maxDepth": {
                        "title": "🔁 Max hop depth",
                        "minimum": 0,
                        "maximum": 4,
                        "type": "integer",
                        "description": "🔁 **How many hops deep to crawl.** Depth 0 = seed only. Depth 1 = seed + its direct related videos. Depth 2 = seed + neighbors-of-neighbors. Depth 3 = three hops out (~500-1100 rows per seed with branch=10).\n\nGeometric growth: rows ≈ `1 + branch + branch² + ... + branch^maxDepth`. The `maxNodes` cap below stops runaway crawls.\n\n- **Default:** 1 (~10 rows)\n- **Min:** 0 (seed only — emits a single row, useful for ID validation)\n- **Max:** 4 (use with low branch and tight maxNodes)",
                        "default": 1
                    },
                    "maxBranchPerNode": {
                        "title": "🌿 Max branch factor per node",
                        "minimum": 1,
                        "maximum": 20,
                        "type": "integer",
                        "description": "🌿 **How many related videos to follow from each crawled node.** Higher = wider local sampling; lower = narrower, deeper chains.\n\n- `1` = follow only the top related video per node — gives chain-like exploration\n- `10` = balanced topical neighborhood (default)\n- `20` = full breadth (use sparingly — each /related call returns ~20 items)\n\n💡 **TIP:** For a topical-cluster study, branch=10 is the sweet spot. For chain-style algorithm tracing, set branch=3 with depth=4.",
                        "default": 10
                    },
                    "maxNodes": {
                        "title": "🛡 Max nodes (safety cap)",
                        "minimum": 1,
                        "maximum": 5000,
                        "type": "integer",
                        "description": "🛡 **Hard cap on total unique videos discovered across the crawl.** BFS stops the moment this cap is reached, even mid-level.\n\nProtects against geometric explosion: depth=4 × branch=10 = up to 11,111 nodes without a cap. Keeping `maxNodes` realistic prevents quota burn.\n\n- **Default:** 500 (covers most depth=2 crawls + most depth=3 crawls with branch=10)\n- **Min:** 1\n- **Max:** 5,000",
                        "default": 500
                    },
                    "maxPagesPerNode": {
                        "title": "📄 Max continuation pages per node",
                        "minimum": 1,
                        "maximum": 5,
                        "type": "integer",
                        "description": "📄 **How many `/related` continuation pages to pull per crawled node** (each page returns ~20 fresh items). Multiplies the local sample at each hop without going deeper.\n\n- **Default:** 1 (single page per node — typical)\n- **Min:** 1\n- **Max:** 5 (~100 items per node)\n\n💡 **TIP:** Leave at 1 unless you specifically want a fatter local neighborhood from the seed.",
                        "default": 1
                    },
                    "includeShorts": {
                        "title": "🩳 Include Shorts from `shorts_listing` carousels",
                        "type": "boolean",
                        "description": "🩳 **When the upstream returns a `shorts_listing` carousel** (e.g. \"Shorts remixing this video\"), unpack and emit each Short as a row.\n\n- `true` = emit Shorts (default)\n- `false` = skip Shorts; only emit standard `video` items\n\nShorts are NOT traversed (no `/related` call is made on them) — they are leaf nodes in the graph. Only the carousel videos themselves are emitted.",
                        "default": true
                    },
                    "includePlaylists": {
                        "title": "📋 Include playlist items",
                        "type": "boolean",
                        "description": "📋 **When the upstream returns `playlist` items** in the related-videos list (e.g. \"Mix - Artist Name\"), emit them as leaf rows.\n\n- `true` = emit playlist items as rows (the cover videoId is captured)\n- `false` = skip playlists entirely (default — playlists are not real related-video nodes; their cover videoId is just a thumbnail anchor)\n\nPlaylists are NEVER traversed (no `/related` call is made on the cover videoId).",
                        "default": false
                    },
                    "geo": {
                        "title": "🌍 Country (optional, ISO 3166-1 alpha-2)",
                        "type": "string",
                        "description": "🌍 **Optional.** Two-letter country code to localize the recommendation graph. Defaults to `US`.\n\nExamples: `US` (United States), `GB` (United Kingdom), `IN` (India), `BR` (Brazil), `DE` (Germany), `JP` (Japan), `KR` (Korea), `MX` (Mexico).",
                        "default": "US"
                    },
                    "lang": {
                        "title": "🗣 Language (optional, ISO 639-1)",
                        "type": "string",
                        "description": "🗣 **Optional.** Two-letter language code for localized labels. Defaults to `en`.\n\nExamples: `en`, `es`, `pt`, `hi`, `ja`, `ko`.",
                        "default": "en"
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
