# Video → LLM Analyzer (`grizzlygriff/video-llm-analyzer`) Actor

Drop a video, give it a prompt, and have an LLM analyze it. Results land in your Notion page so Claude Code can pick up where you left off. Uses Apify MCP Connectors to write to Notion on your behalf — no Notion API key required.

- **URL**: https://apify.com/grizzlygriff/video-llm-analyzer.md
- **Developed by:** [Griffin Trent](https://apify.com/grizzlygriff) (community)
- **Categories:** Videos, Agents, AI
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, NaN bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.008 / analysis completed

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## Let an LLM watch your video

**You give it a video. You tell it what you want. It watches and writes back.**

> **🟢 New here? No setup needed for your first run.** On the **Input** tab: paste a YouTube / TikTok / X / Instagram link (or drop a video file) → keep the example prompt → turn on **"Just give me the analysis"** (so you don't have to connect anything) → click **Start**. Your analysis comes back on the **Output** tab in ~30 seconds.
>
> *The "destination" and "Connector" fields are optional — they auto-write results into an app like Notion or Slack. (An **MCP Connector** is just a one-time, secure link to such an app; Apify never sees its password or token.) You don't need one to try this — that's exactly what "Just give me the analysis" is for.*

That's it. Drop in a video file or paste a link from one of the most popular video platforms (YouTube, TikTok, X, Instagram), tell Claude or GPT what to look for, and get a structured analysis — with timestamps and quotes — back in seconds. The result lands in your dataset (or Notion / Slack / Google Sheets if you connect one), so you or any downstream LLM can keep working with it.

**A 4-minute captioned YouTube run costs a few cents** — about a penny of LLM, plus Apify compute and a flat $0.015 per-run fee. Real example below.

### Why use it

- **Make any LLM video-aware.** Claude, GPT, and Gemini can't watch video natively — this Actor is the bridge. Frames + transcript get fed to the model so it actually has something to reason about.
- **Skip the manual loop.** No more "watch the video → take notes → paste into Notion." One run handles the whole pipe.
- **Built for agents.** Set `skipDestination: true` and the full analysis comes back in the run's OUTPUT — perfect for Claude Code or any agentic workflow that wants to keep going.
- **No LLM API keys needed.** Routes through Apify's hosted OpenRouter integration (OpenRouter is a unified API that proxies Anthropic, OpenAI, and Google models behind a single endpoint). You pay Apify at passthrough rates (~$0.01–$0.10 per video). Bring your own Anthropic key only if you want direct billing or Anthropic-specific features.

### How to use it

1. **Drop in a video.** Either upload a file (MP4/MOV/WebM, ≤1 GB, ≤10 min) or paste a URL from a supported platform.
2. **Write a prompt.** *"Summarize this in 5 bullets with timestamps."* *"List every product mentioned."* *"Describe each scene and who's speaking."*
3. **Pick where the result goes — or skip connecting.** No connection yet? Turn on **"Just give me the analysis"** and the result comes back in the dataset row + OUTPUT — no destination needed. Or pick a [Notion / Slack / Google Sheets MCP Connector](https://docs.apify.com/platform/integrations/mcp-connectors) to auto-write the result. **Notion is fully tested**; Slack and Sheets are functional but lighter-tested — file any rough edges in the Issues tab. *GitHub and Supabase are candidates for future support — file a +1 on the Issues tab if you'd use them.*
4. **Click Start.** A 4-min YouTube video finishes in ~30 seconds. A 10-min file upload takes 1–2 minutes.

**Run this more than once?** Save the filled-in input as a [Task](https://docs.apify.com/platform/actors/running/tasks) — keys and destinations carry over, you only swap in the new video URL each time. Tasks also support scheduling and webhooks.

### Supported video sources

| Input | What works |
|---|---|
| **File upload** | Any MP4/MOV/WebM video file, regardless of which platform it came from. ≤1 GB, ≤10 min. |
| **URL** | The most popular video platforms: YouTube + Shorts, TikTok, X (Twitter), Instagram. |

**Anything else** — Vimeo, Facebook, Reddit, Twitch, Dailymotion, Bilibili, VK, and other sites — needs to be downloaded first and uploaded as a file. The Actor detects unsupported URLs in about a second and fails fast with a clear message, so you're not charged for compute that was never going to work.

Why the short URL list? Generic yt-dlp downloaders are unreliable on cloud infrastructure. We dispatch to platform-specific Actors that actually work. We'll add more platforms as we find reliable backends for them.

### Input

| Field | Required | Description |
|---|---|---|
| **Video** | Yes | File upload OR URL from a supported platform. |
| **Prompt** | Yes | Plain-English instructions. The more specific, the better. |
| **LLM provider** | No | Claude (Anthropic, default), GPT (OpenAI), or Gemini (Google). |
| **Model** | No | `Default` picks the standard model for the provider above, or pick a specific SKU (Sonnet 4.5, Opus 4, Haiku 4.5, GPT-4o, GPT-5, Gemini 2.5 Pro, etc.). ⚠️ **If the model's family doesn't match the LLM provider above, the model wins** — e.g. provider=Claude + model=`openai/gpt-4o` bills through the OpenAI route, not Claude. Keep them matching, or just leave on `Default`. |
| **Anthropic API key** | No | Only used if Provider=Claude. When set, calls Anthropic directly with your key (instead of Apify's OpenRouter passthrough) — useful for prompt caching, extended thinking, or billing your own account. |
| **Whisper API key** | No | OpenAI key for audio transcription. Skip unless the analysis genuinely depends on what people said — frames + metadata cover most prompts. ~$0.006/min of audio. |
| **Destination** | No | MCP Connector — pick **Notion** (fully tested), **Slack** (functional, lighter-tested), or **Google Sheets** (functional, lighter-tested). Leave empty + set `skipDestination: true` to just get the analysis back in the dataset. |
| **Destination target** | Conditional | Page URL (Notion), channel (Slack), or spreadsheet URL (Sheets). |
| **Skip destination** | No | When `true`, returns the analysis in the dataset row + OUTPUT and writes nowhere. Best for agentic callers. |
| **Frames to extract** | No | `0` = adaptive (~1 frame per 5s, 4–12 frames). Override only if you have a specific reason. |
| **Force fresh run** | No | By default, identical inputs replay the prior result instead of re-paying for the LLM call. Set `true` to force a fresh analysis. |
| **Maximum cost for this run (USD)** | No | Hard ceiling on what this run can cost you. After ingest, the Actor computes a worst-case cost and refuses the LLM call (no LLM cost incurred) if it would exceed this number. Leave at 0 for no cap. Anchor against the Pricing table below — most YouTube runs cost under $0.05; a 10-min upload tops out around $0.17. Set $0.20 for headroom, $0.50 for long uploads. |

### Output

Every run produces one dataset row + one `OUTPUT` key-value-store record with the **same full payload** (documented below), plus a separate `ANALYSIS` record holding just the clean Markdown analysis (`{ "analysis": "<markdown>" }`) for when you only want the text. The run's **Output tab** in Console surfaces these as quick links — *Read your analysis* (the `ANALYSIS` record), *Full result record* (the `OUTPUT` record), the run + history table, and the run log. Use whichever fits how you're calling the Actor.

#### Output schema

| Field | Type | Description |
|---|---|---|
| `status` | string | `succeeded`, `succeeded_no_url`, `succeeded_skipped_destination`, `succeeded_idempotent_replay`, or `failed_*`. |
| `completionTimestamp` | string | ISO-8601 timestamp of when the run finished. |
| `writeBehavior` | string | `append` or `nested` — the behavior the Actor used for this run. |
| `writeMode` | string | The literal action performed: `notion_append`, `notion_child_page`, `slack_message`, `sheets_row`, or `unknown` (when destination skipped). |
| `routingTrigger` | string \| null | If the write behavior was decided from your prompt, the phrase that matched (e.g. `"new notion page"`). `null` when behavior came from the input or default. |
| `destinationUrl` | string | URL of the page/channel/sheet the analysis was written to. Empty when `skipDestination: true`. |
| `destinationService` | string | `notion`, `slack`, `sheets`, or `skipped`. |
| `analysisMode` | string | How the video was read: `transcript+thumbnail` (YouTube captions path — cheapest, one still image), `frames` (multi-frame visual, no spoken-word transcript), `frames+transcript` (multi-frame visual **plus** captions — e.g. a captioned YouTube video analyzed with a visual prompt), or `frames+audio` (multi-frame visual plus a Whisper audio transcript). |
| `sourceUrl` | string \| null | Original URL if you pasted one, else `null`. |
| `youtubeTitle` | string \| null | Video title, populated for YouTube inputs. |
| `youtubeChannel` | string \| null | Channel name, populated for YouTube inputs. |
| `framesUsed` | integer | Number of frames the LLM analyzed. `1` in `transcript+thumbnail` mode (the thumbnail). |
| `durationSec` | number \| null | Video length in seconds, when known (file mode + download fallback). |
| `provider` | string | `anthropic`, `openai`, or `gemini`. |
| `model` | string | Specific model SKU used (e.g. `anthropic/claude-sonnet-4.5`). |
| `whisperUsed` | boolean | Whether audio was transcribed via Whisper (vs. native captions or no audio). |
| `transcriptChars` | integer | Length of the transcript fed to the LLM. |
| `tokensIn` | integer | LLM input tokens. |
| `tokensOut` | integer | LLM output tokens. |
| `estimatedCostUsd` | number | Actual LLM + Whisper cost based on tokens used. Excludes Apify compute. |
| `estimatedCostUsdMax` | number | Worst-case cost computed before the LLM call (used by the budget ceiling). |
| `idempotencyKey` | string | Hash of `prompt + source + destinationTarget + writeBehavior + provider + frames`. Used to short-circuit duplicate runs. **Note:** `destinationTarget` (the page URL / channel / sheet) IS in the hash; the chosen Connector ID is NOT. Two runs with the same `destinationTarget` but different Connectors will share an idempotency key — a replay returns the prior `destinationUrl` without re-checking the new Connector. Set `forceFreshRun: true` after swapping Connectors. |
| `analysisPreview` | string | First 500 characters of the LLM analysis. |
| `analysisFull` | string | Full LLM analysis text. |
| `softErrors` | object \| null | `null` on clean runs. When any ingest step degraded, this field is a small object with keys `whisper`, `youtubeTranscript`, `youtubeDownloaderFallback`, and `youtubeVisualFallback` — each a human-readable sentence if it tripped, else `null`. `youtubeVisualFallback` is set when a **visual prompt on a YouTube video** couldn't get real frames (the clip was over the 10-minute download cap, or its length couldn't be confirmed) and the run used the transcript + thumbnail instead — so even a `succeeded` run tells you why a "describe the scenes" prompt came back transcript-based. Agent callers can check `softErrors != null` to detect partial- or degraded-analysis cases. |
| `timings` | object \| null | Per-stage durations in milliseconds — `youtubeFetchMs`, `downloadMs`, `frameExtractMs`, `whisperMs`, `llmMs`, `mcpWriteMs`. Each key is `null` if the stage didn't run on this path (e.g. `whisperMs: null` when no Whisper key was set; `downloadMs: null` on YouTube-with-captions runs that skip the file pipeline). Whole field is `null` on idempotent-replay runs. **`mcpWriteMs` is total-elapsed across all retry attempts (including backoff sleeps)**, not last-attempt time — useful for detecting retry storms, not for measuring proxy latency. Pipeline operators can alert on `timings.whisperMs > 30000` or `timings.llmMs p95 > 15000` without parsing logs. Stage timings are captured on both success AND failure paths, so a Whisper call that runs for 28s and then raises still shows `whisperMs: 28000`. |
| `errorCategory` | string \| absent | Only present on failed runs. **Canonical, closed enum** — agent callers should branch on this instead of `status_message`. Recoverable categories (safe to retry as-is): `download_failed_infra`, `download_no_url`, `pipeline_failed`, `llm_failed`, `write_failed`. Permanent categories (require input change): `invalid_input`, `unsupported_platform`, `unsupported_destination`, `download_video_gone`, `budget_exceeded`, `internal_error`. |
| `errorRecoverable` | boolean \| absent | Only present on failed runs. `true` if a retry of the same input might succeed; `false` if the user must change the input first. |
| `errorRetryAfterSec` | integer \| absent | Only present when `errorRecoverable: true` and the failure is rate-limited or otherwise time-sensitive. Number of seconds to wait before retrying. Today this is populated only for `write_failed` (30s) and `download_failed_infra` (60s) — other recoverable categories don't have a meaningful retry-after. |
| `destinationFailureKind` | string \| absent | Only present on `errorCategory: "unsupported_destination"` runs. One of `"unsupported"` (the picked Connector exposes no matching writers) or `"auth_or_dead"` (the proxy returned a non-retryable 4xx). Lets agent callers route the two recovery actions (re-pick Connector vs. re-authorize) without parsing `status_message`. |
| `destinationFailureHttpCode` | integer \| null \| absent | Only present on `errorCategory: "unsupported_destination"` runs. `null` when `destinationFailureKind == "unsupported"` (no HTTP call was made — the Connector responded but with no matching tools). A 4xx integer (e.g. `401`, `403`) when `destinationFailureKind == "auth_or_dead"`. Type as `Optional[int]` in Pydantic models. |
| `destinationToolsExposed` | string[] \| absent | Only present on `errorCategory: "unsupported_destination"` runs. The tools the picked Connector actually exposed, sorted alphabetically — useful for diagnostic logging (detect new Connector types proliferating on Apify's marketplace) or for synthesizing a "did you mean Notion?" suggestion without making a second MCP round-trip. |

**Note on `status` vs `errorCategory`:** `status` (open string, e.g. `"failed_invalid_input"`) is the legacy classifier kept for backward compatibility. **`errorCategory` is the canonical closed enum** going forward. New agent integrations should branch on `errorCategory`; treat `status` as informational.

**Retry-policy example for agent callers:**

```python
result = await run_actor(...)  ## one dataset row per run

if result.get("status") == "succeeded":
    return result["analysisFull"]

## Failed. Branch deterministically on errorCategory:
if not result.get("errorRecoverable"):
    raise PermanentError(f"{result['errorCategory']}: {result['statusMessage']}")

## Recoverable. Honor the retry-after hint when present.
wait_sec = result.get("errorRetryAfterSec") or 10
await sleep(wait_sec)
return await run_actor(...)  ## retry with same input
````

#### Example output

Real output from this Actor running on [Build on Apify (Part 1): From Idea to Solution](https://www.youtube.com/watch?v=_f5VEaR-F2k) (4:06, Apify channel) with the prompt *"Summarize this video in 5 bullets. For each bullet, include a rough timestamp (e.g. 0:42) and quote any spoken line that matters."*

```json
{
  "status": "succeeded_skipped_destination",
  "completionTimestamp": "2026-05-28T14:45:45.607228+00:00",
  "writeBehavior": "append",
  "writeMode": "unknown",
  "routingTrigger": null,
  "destinationUrl": "",
  "destinationService": "skipped",
  "analysisMode": "transcript+thumbnail",
  "sourceUrl": "https://www.youtube.com/watch?v=_f5VEaR-F2k",
  "youtubeTitle": "Build on Apify (Part 1): From Idea to Solution",
  "youtubeChannel": "Apify",
  "framesUsed": 1,
  "durationSec": null,
  "provider": "anthropic",
  "model": "anthropic/claude-sonnet-4.5",
  "whisperUsed": false,
  "transcriptChars": 1373,
  "tokensIn": 1903,
  "tokensOut": 299,
  "estimatedCostUsd": 0.0102,
  "estimatedCostUsdMax": 0.0358,
  "idempotencyKey": "ff0dfcc9d6795d85",
  "analysisPreview": "## Video Summary: Build on Apify (Part 1): From Idea to Solution\n\nBased on the transcript and timestamps provided, here are 5 key bullets summarizing this video:\n\n• **0:00 - Introduction to the series**: This is Part 1 of a 5-part series...",
  "analysisFull": "## Video Summary: Build on Apify (Part 1): From Idea to Solution\n\nBased on the transcript and timestamps provided, here are 5 key bullets summarizing this video:\n\n• **0:00 - Introduction to the series**: This is Part 1 of a 5-part series that guides developers through creating web automation tools on Apify, from initial concept to publishing on the Apify Store for passive income.\n\n• **0:59 - What you can build**: The video explains Apify's use cases, including \"Web Scrapers: Collect product, price, or lead data from any website\" and \"Monitoring tools: Watch for changes in job listings, hotel prices, or real estate offers.\"\n\n• **2:00 - From idea to Actor**: This section covers the process of developing an Actor (Apify's term for automation tools), walking through how to transform a simple idea into a working solution.\n\n• **3:20 - Managing your tools**: The video discusses how to manage and maintain your Actors once they're built, including running and scaling them in the cloud.\n\n• **3:40 - Conclusion and next steps**: The video wraps up Part 1 and promotes the Apify $1M Challenge where developers can \"get rewards for building Actors,\" encouraging viewers to start building and monetizing their automation tools."
}
```

The **LLM portion** of this run was **$0.0102** — just over a penny — because the video had native YouTube captions, so the Actor skipped frame extraction and Whisper entirely and ran the cheapest path. Your full bill adds Apify compute (~$0.01) and the flat $0.015 per-run fee, landing around **$0.035 total**.

### Pricing

**Total per-run cost** — three things land on your Apify bill: Apify compute + LLM passthrough + this Actor's per-run fee (a flat **$0.015**: $0.005 to start + $0.008 per analysis + $0.002 per destination write). Defaults use **Claude Sonnet 4.5**, so figures below assume Claude; Gemini Flash or GPT-4o-mini run noticeably cheaper on the LLM line.

| Input | Total | Breakdown |
|---|---|---|
| YouTube with captions (any length ≤10 min) | **~$0.03–$0.07** | LLM ($0.01–$0.03 — captions skip Whisper and frame extraction) + Apify compute (~$0.01–$0.02) + Actor fee ($0.015) |
| YouTube no-captions, 2 min | **~$0.12** | compute + download + Whisper + LLM + Actor fee |
| TikTok / X / Instagram, 1 min | **~$0.08** | compute + download + Whisper + LLM + Actor fee |
| 30-sec file upload | **~$0.06** | compute + Whisper + LLM + Actor fee |
| 10-min file upload (worst case) | **~$0.19** | compute ($0.02) + Whisper for 10 min ($0.06) + LLM for full transcript + 12 frames on Claude Sonnet ($0.09) + Actor fee ($0.015) |

*Skipping the destination (agentic/API callers) drops the Actor fee to $0.013. Re-running an identical prompt+video replays from cache and charges the $0.005 start only — no LLM cost.*

The Actor logs an **estimated total** in the run's status line before any LLM call fires, so you see the number before any cost is committed. To hard-cap a run, set a budget ceiling — there are two surfaces and whichever is tighter wins:

- **Input form** — fill in *Maximum cost for this run (USD)*. More discoverable on a per-run basis.
- **Apify Run dialog** — set *Maximum total charge in USD*. Platform-standard, applies to any Actor.

If the worst-case estimate would exceed either cap, the Actor refuses to start the LLM call and the run ends with **no LLM cost incurred**.

**Why three charge lines on your Apify invoice?** Apify charges you (a) compute units for the container time, (b) pass-through events for the LLM tokens consumed via OpenRouter, and (c) this Actor's per-run fee ($0.005 start + $0.008 per analysis + $0.002 per destination write). All land on your Apify account — there's no separate Anthropic / OpenAI bill (unless you opt in to the Anthropic API key field for direct billing). Note the *Maximum cost* cap below limits the **LLM portion** the Actor controls before firing; compute and the per-run fee are billed on top.

### Troubleshooting

| Symptom | What's going on | Fix |
|---|---|---|
| **"Unsupported platform"** at the start of a run | URL platform isn't in the supported list (Vimeo, Facebook, Reddit, Twitch, Dailymotion, Bilibili, VK, etc.). | Download the video locally first, then upload the MP4 to the file field. |
| **"Video exceeds 1 GB"** | Hard cap to bound LLM cost. | Re-encode at lower bitrate (HandBrake, ffmpeg `-crf 28`) or trim to the relevant section. |
| **"Video exceeds 10 minutes"** | Hard cap. Whisper + frame budgets grow fast past this. | Chunk into ≤10-min pieces and run separately. Combine results manually. |
| **Analysis is empty or vague** | LLM didn't have enough signal — likely silent video with no captions and no Whisper key. | Add a [Whisper key](https://platform.openai.com/api-keys) so the audio gets transcribed. |
| **Visual prompt on a YouTube video came back describing speech, not scenes** | The video was longer than 10 minutes (or its length couldn't be confirmed), so real frames couldn't be downloaded — the run used the transcript + thumbnail. The OUTPUT `softErrors.youtubeVisualFallback` field and the run log say so. | Download the video and upload it as a file — the file path always extracts real frames. Or shorten the clip to ≤10 minutes. |
| **"Destination write failed"** | MCP Connector lost permissions or the target URL is wrong. | Re-open the input form, re-pick the Connector, confirm the page/channel/sheet URL still resolves, re-run. Your LLM cost from the prior run is cached — the retry only pays for the destination write. |
| **"The Connector you picked doesn't expose the tools this Actor needs"** | You selected a Connector the Apify Console marks **'Not eligible'** — usually GitHub / Supabase / Sentry / another non-supported MCP server. We catch this before the LLM call so **no LLM cost is incurred**. (The fixed `actor-start` compute fee — ~$0.02 per run at 4 GB memory — still applies, as on any run that reaches the Actor.) | Re-pick a Notion, Slack, or Google Sheets Connector — or toggle **Skip destination** if you just want the analysis back in the dataset row. |
| **"Before any LLM work: couldn't reach the Connector you picked"** | The Connector you picked exists, but its authorization expired or the underlying resource was deleted. We caught it at the gate — **no LLM cost is incurred**. (The fixed `actor-start` compute fee still applies as above.) | Re-authorize the Connector in Apify Console → Settings → Integrations → MCP Connectors, then re-run. |
| **Run succeeded but nothing appeared in Notion** | Probably ran with `skipDestination: true`. | Open the run, click OUTPUT — the analysis is in the `analysisFull` field. |
| **"Idempotent replay"** in the status | Identical inputs to a previous successful run — the prior result was replayed instead of re-paying for the LLM. | Set `forceFreshRun: true` in the input to force a fresh analysis. **Replays return the prior `destinationUrl`** even if you've since revoked the original Connector — the URL is historical, not re-verified. Agent callers building user-facing surfaces should treat the URL's freshness as best-effort and HEAD-check it before exposing to end users. |
| **"API key rejected"** within seconds of starting | Bad/expired Anthropic key. | Regenerate at [console.anthropic.com/settings/keys](https://console.anthropic.com/settings/keys), paste the new one. |

### Tips

- **YouTube with captions is by far the cheapest path** — it skips video download, Whisper, and frame extraction entirely. The example above ran for about a penny.
- **For best fidelity, upload the file directly.** The file pipeline always extracts real frames + Whisper transcript — the highest-quality "watch" path.
- **Skip the Whisper key if the video has no useful audio** (silent screen recording, B-roll). Saves time and money.
- **Set `frames` to 4–8 for short talking-head clips.** Adaptive mode picks 4–12 frames; pinning to a low number for stable-scene videos cuts LLM cost 50–80% with no quality loss.
- **Reuse the same Notion page across runs** — each one appends a new section, so the page becomes a running video-analysis log you can scroll back through.

### FAQ

**Who sees my video frames and transcript?** The Actor extracts frames + (optionally) a Whisper transcript and sends them to the LLM provider you pick. By default that's **OpenRouter** (which routes to Anthropic, OpenAI, or Google depending on the model). If you set a Whisper key, audio is sent to OpenAI for transcription. Both Apify and the LLM/Whisper provider may retain operational logs per their privacy policies — **don't submit confidential or regulated data.** Apify's destination MCP proxy injects your Notion/Slack/Sheets token server-side; the Actor itself never sees that token.

**Why a 10-minute cap?** Whisper costs and frame budgets scale linearly with length. The cap keeps per-run cost predictable and within the pricing table above. If you need longer videos, chunk and stitch manually.

**Can I use my own LLM key instead of Apify's OpenRouter?** Yes — paste an Anthropic key in the optional field. It only applies when Provider=Claude. GPT and Gemini always go through OpenRouter (no BYO key path for them yet).

**Bug or feature request?** Open an issue on the Actor's **Issues** tab in Apify Console.

### Legal & responsible use

**You are responsible** for ensuring you have the right to analyze any video you submit. Submitting copyrighted material, content depicting identifiable individuals without their consent, or content prohibited by the source platform's Terms of Service is your responsibility — not Apify's, and not this Actor's authors'. Apify reserves the right to suspend accounts that use this Actor in ways that violate the [Apify Acceptable Use Policy](https://docs.apify.com/legal/acceptable-use-policy).

**No automated content moderation.** Frames and audio are passed to the LLM provider you select (Anthropic, OpenAI via OpenRouter, or Google). Do not submit illegal content, CSAM, or content prohibited by your LLM provider's usage policy — your provider's safety filter may refuse the response, and any violation is logged against your account.

**Trademarks & affiliations.** This Actor is **not affiliated with, endorsed by, or sponsored by** Anthropic, OpenAI, OpenRouter, Google, Google Sheets, Whisper, Notion, or Slack. Product names and trademarks are referenced for technical compatibility only.

# Actor input Schema

## `video` (type: `string`):

**New here?** Drop a video (or paste a link) here and keep the example prompt. To run with no setup, turn on **'Just give me the analysis'** in step 3 below, then click **Start**.

**Easiest path: drop a file** (MP4/MOV/WebM, ≤1 GB, ≤10 min). Any video file works regardless of where it came from.

**Or paste a URL from one of these platforms:** YouTube + Shorts, TikTok, X (Twitter), Instagram.

Anything else — Vimeo, Dailymotion, Facebook, Reddit, Twitch, Bilibili, VK, and any other site not listed above — needs to be downloaded and uploaded as a file. We can't reliably download from these from the cloud yet. If you paste an unsupported URL, we'll tell you in about a second — before any compute runs, so you're not charged.

## `prompt` (type: `string`):

Plain English. *"Summarize the main points"*, *"List every product mentioned with timestamps"*, *"Describe each scene and who's speaking."* The more specific, the better the result.

**For YouTube links:** a visual prompt — one that mentions scenes, frames, what's *on screen*, who appears, etc. — automatically pulls real video frames (not just the thumbnail) **as long as the clip is 10 minutes or shorter**. For longer YouTube videos we can't download frames, so a visual prompt falls back to the transcript + thumbnail (the run says so in `softErrors` and the log); to force full-frame analysis on a long video, download it and upload the file instead. Purely spoken-word prompts always use the faster, cheaper transcript path.

## `llmProvider` (type: `string`):

Which model family analyzes the video. **No API keys needed** — this Actor routes through Apify's hosted OpenRouter integration, so you only pay your Apify account at OpenRouter's actual cost (rounded to the nearest $0.00001). Pick the family here; for a specific model override, see the Model field below.

## `model` (type: `string`):

Pick **Default** to use the provider's standard model — Claude Sonnet 4.5, GPT-4o mini, or Gemini 2.5 Flash, depending on the provider above. Or pick a specific model. ⚠️ **If your model is from a different family than the provider above** (e.g. provider=Claude + model=`openai/gpt-4o`), the model's family wins and the run bills through that path. Keep the model family matching the provider, or leave on Default.

## `anthropicApiKey` (type: `string`):

**Only used when the provider above is Claude. Ignored for GPT and Gemini.** Skip this unless you want to spend your own Anthropic credits instead of Apify's hosted OpenRouter (the default).

When filled AND the provider is Claude, the call hits Anthropic directly with your key — useful for Anthropic-specific features (prompt caching, extended thinking) or to bill your own Anthropic account instead of Apify's OpenRouter passthrough. The key lives in this Actor's memory for the run and is then discarded. Get a key at [console.anthropic.com/settings/keys](https://console.anthropic.com/settings/keys).

## `whisperKey` (type: `string`):

**Skip this — most prompts don't need spoken words.** Frames + the video's metadata cover ~90% of analysis prompts. Add a Whisper key (paste an OpenAI key from [platform.openai.com/api-keys](https://platform.openai.com/api-keys)) only when the analysis genuinely depends on what people *said*. ~$0.006 per minute of audio.

This is the one piece that still needs a raw API key for now — no MCP server wraps Whisper yet.

## `skipDestination` (type: `boolean`):

**Turn this on to get the analysis back without connecting anything** — recommended for your first run, when you don't have a Notion / Slack / Sheets connection yet, or when calling from an agent / API / Claude Code. **Leaving the destination below empty without turning this on will stop the run and ask for a destination.** When on, the destination + page/channel/sheet + write-behavior fields below are ignored and the analysis lands in this run's dataset row and OUTPUT record.

## `destination` (type: `string`):

**Optional. To run with no connection, turn on 'Just give me the analysis' above and leave this empty.** Otherwise, pick a saved connection to auto-write results into **Notion** (fully tested), **Slack**, or **Google Sheets** (both functional, lighter-tested).

Don't have a connection? Click the field → **Create new** → log in to the app and authorize. Seeing Connectors tagged **'Not eligible'**? Those are for other services (GitHub, Supabase, Sentry…) that don't write to Notion/Slack/Sheets — picking one is safe, we catch it before the LLM call and exit with **no LLM cost incurred**.

## `destinationTarget` (type: `string`):

Where exactly within the connection. Format depends on what you picked:

| If you picked… | Paste this | Example |
|---|---|---|
| **Notion** | Page URL (not a database) | `https://www.notion.so/My-Notes-1234abcd5678` |
| **Slack** | Channel name or ID | `#video-notes` or `C012AB3CD` |
| **Google Sheets** | Spreadsheet URL | `https://docs.google.com/spreadsheets/d/<id>/edit` |

Slack needs the Apify Slack app in the channel. Notion appends a section by default, or creates a new child page if your prompt asks for one. Sheets always appends a row.

## `writeBehavior` (type: `string`):

**Default (Smart) is right for nearly everyone.** Reads your prompt — if you said *"new page"* / *"separate section"*, creates a new container; otherwise appends to what's there. Pin one of the others if you want to override prompt-detection.

## `framesToExtract` (type: `integer`):

Leave at 0 for adaptive (~1 frame per 5s, between 4 and 12 frames). Override only if you have a specific reason — more frames = higher LLM cost. **Note:** very high frame counts can produce a payload too large for the LLM to accept; if you set this higher than 12 the Actor will cap it at 12 and log a notice.

**YouTube tip:** captioned YouTube videos are normally read via transcript + a single thumbnail (cheapest). Setting this above 0 — or writing a visual prompt (see the **prompt** field above) — makes the Actor download the video and extract real frames instead, as long as the clip is within the 10-minute limit. On a captioned video this keeps the captions too, so you get frames **and** transcript.

## `forceFreshRun` (type: `boolean`):

By default, if you re-run with the **exact same** prompt + video + destination + behaviour as a previous run, the Actor replays the previous result instead of paying for the LLM call again. Set this to `true` to force a fresh analysis — useful if the source video has been re-uploaded under the same URL or you want a different angle on the same clip.

## `maxChargeUsd` (type: `number`):

**Set a ceiling on the LLM + Whisper cost of this run.** When the Actor finishes ingest and knows the real frame/transcript size, it computes a worst-case LLM+Whisper cost. If that estimate exceeds this cap, the run aborts *before* the LLM call fires — no LLM cost incurred. **This caps only the LLM/Whisper portion** — Apify compute and the flat $0.015 per-run fee are billed on top (see the README pricing table). Leave at 0 for no cap. As a guide, the LLM+Whisper portion is under $0.05 for most captioned YouTube videos and tops out around $0.15 for a 10-min upload on Claude Sonnet. Set $0.20 for headroom on typical videos; $0.50 for long uploads.

## Actor input object example

```json
{
  "prompt": "Summarize this video in 5 bullets. For each bullet, include a rough timestamp (e.g. 0:42) and quote any spoken line that matters.",
  "llmProvider": "anthropic",
  "model": "default",
  "skipDestination": false,
  "writeBehavior": "auto",
  "framesToExtract": 0,
  "forceFreshRun": false,
  "maxChargeUsd": 0
}
```

# Actor output Schema

## `analysis` (type: `string`):

The deliverable: the full LLM analysis as Markdown text, under the `analysis` field. This is the clean, human-readable result — open this first.

## `fullOutput` (type: `string`):

The complete machine-readable object: `analysisFull` plus every run field — source URL, destination URL, provider, model, token counts, estimated cost, timings, and any soft errors. Use this when calling the Actor from an agent or API.

## `dataset` (type: `string`):

This run's row plus every future one — date, source, destination URL, cost, preview. Switch the view dropdown to **Diagnostic (all fields)** for full per-run details.

## `log` (type: `string`):

Open if a run failed, came back shorter than expected, or you want to see which step was slow.

# 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 = {
    "prompt": "Summarize this video in 5 bullets. For each bullet, include a rough timestamp (e.g. 0:42) and quote any spoken line that matters."
};

// Run the Actor and wait for it to finish
const run = await client.actor("grizzlygriff/video-llm-analyzer").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 = { "prompt": "Summarize this video in 5 bullets. For each bullet, include a rough timestamp (e.g. 0:42) and quote any spoken line that matters." }

# Run the Actor and wait for it to finish
run = client.actor("grizzlygriff/video-llm-analyzer").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 '{
  "prompt": "Summarize this video in 5 bullets. For each bullet, include a rough timestamp (e.g. 0:42) and quote any spoken line that matters."
}' |
apify call grizzlygriff/video-llm-analyzer --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=grizzlygriff/video-llm-analyzer",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Video → LLM Analyzer",
        "description": "Drop a video, give it a prompt, and have an LLM analyze it. Results land in your Notion page so Claude Code can pick up where you left off. Uses Apify MCP Connectors to write to Notion on your behalf — no Notion API key required.",
        "version": "2.7",
        "x-build-id": "YYxA7rNL0eh7hsfTc"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/grizzlygriff~video-llm-analyzer/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-grizzlygriff-video-llm-analyzer",
                "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/grizzlygriff~video-llm-analyzer/runs": {
            "post": {
                "operationId": "runs-sync-grizzlygriff-video-llm-analyzer",
                "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/grizzlygriff~video-llm-analyzer/run-sync": {
            "post": {
                "operationId": "run-sync-grizzlygriff-video-llm-analyzer",
                "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",
                "required": [
                    "video",
                    "prompt"
                ],
                "properties": {
                    "video": {
                        "title": "Video",
                        "type": "string",
                        "description": "**New here?** Drop a video (or paste a link) here and keep the example prompt. To run with no setup, turn on **'Just give me the analysis'** in step 3 below, then click **Start**.\n\n**Easiest path: drop a file** (MP4/MOV/WebM, ≤1 GB, ≤10 min). Any video file works regardless of where it came from.\n\n**Or paste a URL from one of these platforms:** YouTube + Shorts, TikTok, X (Twitter), Instagram.\n\nAnything else — Vimeo, Dailymotion, Facebook, Reddit, Twitch, Bilibili, VK, and any other site not listed above — needs to be downloaded and uploaded as a file. We can't reliably download from these from the cloud yet. If you paste an unsupported URL, we'll tell you in about a second — before any compute runs, so you're not charged."
                    },
                    "prompt": {
                        "title": "What should the LLM do with this video?",
                        "minLength": 5,
                        "maxLength": 4000,
                        "type": "string",
                        "description": "Plain English. *\"Summarize the main points\"*, *\"List every product mentioned with timestamps\"*, *\"Describe each scene and who's speaking.\"* The more specific, the better the result.\n\n**For YouTube links:** a visual prompt — one that mentions scenes, frames, what's *on screen*, who appears, etc. — automatically pulls real video frames (not just the thumbnail) **as long as the clip is 10 minutes or shorter**. For longer YouTube videos we can't download frames, so a visual prompt falls back to the transcript + thumbnail (the run says so in `softErrors` and the log); to force full-frame analysis on a long video, download it and upload the file instead. Purely spoken-word prompts always use the faster, cheaper transcript path."
                    },
                    "llmProvider": {
                        "title": "Pick which AI watches the video",
                        "enum": [
                            "anthropic",
                            "openai",
                            "gemini"
                        ],
                        "type": "string",
                        "description": "Which model family analyzes the video. **No API keys needed** — this Actor routes through Apify's hosted OpenRouter integration, so you only pay your Apify account at OpenRouter's actual cost (rounded to the nearest $0.00001). Pick the family here; for a specific model override, see the Model field below.",
                        "default": "anthropic"
                    },
                    "model": {
                        "title": "Model",
                        "enum": [
                            "default",
                            "anthropic/claude-sonnet-4.5",
                            "anthropic/claude-opus-4",
                            "anthropic/claude-haiku-4.5",
                            "openai/gpt-4o",
                            "openai/gpt-4o-mini",
                            "openai/gpt-5",
                            "google/gemini-2.5-flash-lite",
                            "google/gemini-2.5-flash",
                            "google/gemini-2.5-pro"
                        ],
                        "type": "string",
                        "description": "Pick **Default** to use the provider's standard model — Claude Sonnet 4.5, GPT-4o mini, or Gemini 2.5 Flash, depending on the provider above. Or pick a specific model. ⚠️ **If your model is from a different family than the provider above** (e.g. provider=Claude + model=`openai/gpt-4o`), the model's family wins and the run bills through that path. Keep the model family matching the provider, or leave on Default.",
                        "default": "default"
                    },
                    "anthropicApiKey": {
                        "title": "Anthropic API key (optional — for direct billing)",
                        "type": "string",
                        "description": "**Only used when the provider above is Claude. Ignored for GPT and Gemini.** Skip this unless you want to spend your own Anthropic credits instead of Apify's hosted OpenRouter (the default).\n\nWhen filled AND the provider is Claude, the call hits Anthropic directly with your key — useful for Anthropic-specific features (prompt caching, extended thinking) or to bill your own Anthropic account instead of Apify's OpenRouter passthrough. The key lives in this Actor's memory for the run and is then discarded. Get a key at [console.anthropic.com/settings/keys](https://console.anthropic.com/settings/keys)."
                    },
                    "whisperKey": {
                        "title": "Whisper key for audio transcription (optional)",
                        "type": "string",
                        "description": "**Skip this — most prompts don't need spoken words.** Frames + the video's metadata cover ~90% of analysis prompts. Add a Whisper key (paste an OpenAI key from [platform.openai.com/api-keys](https://platform.openai.com/api-keys)) only when the analysis genuinely depends on what people *said*. ~$0.006 per minute of audio.\n\nThis is the one piece that still needs a raw API key for now — no MCP server wraps Whisper yet."
                    },
                    "skipDestination": {
                        "title": "Just give me the analysis — no connection needed",
                        "type": "boolean",
                        "description": "**Turn this on to get the analysis back without connecting anything** — recommended for your first run, when you don't have a Notion / Slack / Sheets connection yet, or when calling from an agent / API / Claude Code. **Leaving the destination below empty without turning this on will stop the run and ask for a destination.** When on, the destination + page/channel/sheet + write-behavior fields below are ignored and the analysis lands in this run's dataset row and OUTPUT record.",
                        "default": false
                    },
                    "destination": {
                        "title": "Auto-save results to Notion / Slack / Sheets (optional)",
                        "type": "string",
                        "description": "**Optional. To run with no connection, turn on 'Just give me the analysis' above and leave this empty.** Otherwise, pick a saved connection to auto-write results into **Notion** (fully tested), **Slack**, or **Google Sheets** (both functional, lighter-tested).\n\nDon't have a connection? Click the field → **Create new** → log in to the app and authorize. Seeing Connectors tagged **'Not eligible'**? Those are for other services (GitHub, Supabase, Sentry…) that don't write to Notion/Slack/Sheets — picking one is safe, we catch it before the LLM call and exit with **no LLM cost incurred**."
                    },
                    "destinationTarget": {
                        "title": "Page, channel, or sheet",
                        "type": "string",
                        "description": "Where exactly within the connection. Format depends on what you picked:\n\n| If you picked… | Paste this | Example |\n|---|---|---|\n| **Notion** | Page URL (not a database) | `https://www.notion.so/My-Notes-1234abcd5678` |\n| **Slack** | Channel name or ID | `#video-notes` or `C012AB3CD` |\n| **Google Sheets** | Spreadsheet URL | `https://docs.google.com/spreadsheets/d/<id>/edit` |\n\nSlack needs the Apify Slack app in the channel. Notion appends a section by default, or creates a new child page if your prompt asks for one. Sheets always appends a row."
                    },
                    "writeBehavior": {
                        "title": "How to write to the destination",
                        "enum": [
                            "auto",
                            "append",
                            "nested"
                        ],
                        "type": "string",
                        "description": "**Default (Smart) is right for nearly everyone.** Reads your prompt — if you said *\"new page\"* / *\"separate section\"*, creates a new container; otherwise appends to what's there. Pin one of the others if you want to override prompt-detection.",
                        "default": "auto"
                    },
                    "framesToExtract": {
                        "title": "Frames to extract",
                        "minimum": 0,
                        "maximum": 30,
                        "type": "integer",
                        "description": "Leave at 0 for adaptive (~1 frame per 5s, between 4 and 12 frames). Override only if you have a specific reason — more frames = higher LLM cost. **Note:** very high frame counts can produce a payload too large for the LLM to accept; if you set this higher than 12 the Actor will cap it at 12 and log a notice.\n\n**YouTube tip:** captioned YouTube videos are normally read via transcript + a single thumbnail (cheapest). Setting this above 0 — or writing a visual prompt (see the **prompt** field above) — makes the Actor download the video and extract real frames instead, as long as the clip is within the 10-minute limit. On a captioned video this keeps the captions too, so you get frames **and** transcript.",
                        "default": 0
                    },
                    "forceFreshRun": {
                        "title": "Force a fresh run (skip duplicate-result shortcut)",
                        "type": "boolean",
                        "description": "By default, if you re-run with the **exact same** prompt + video + destination + behaviour as a previous run, the Actor replays the previous result instead of paying for the LLM call again. Set this to `true` to force a fresh analysis — useful if the source video has been re-uploaded under the same URL or you want a different angle on the same clip.",
                        "default": false
                    },
                    "maxChargeUsd": {
                        "title": "Maximum cost for this run (USD)",
                        "minimum": 0,
                        "type": "number",
                        "description": "**Set a ceiling on the LLM + Whisper cost of this run.** When the Actor finishes ingest and knows the real frame/transcript size, it computes a worst-case LLM+Whisper cost. If that estimate exceeds this cap, the run aborts *before* the LLM call fires — no LLM cost incurred. **This caps only the LLM/Whisper portion** — Apify compute and the flat $0.015 per-run fee are billed on top (see the README pricing table). Leave at 0 for no cap. As a guide, the LLM+Whisper portion is under $0.05 for most captioned YouTube videos and tops out around $0.15 for a 10-min upload on Claude Sonnet. Set $0.20 for headroom on typical videos; $0.50 for long uploads.",
                        "default": 0
                    }
                }
            },
            "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
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
