Bulk video updates¶
The whole reason ytstudio exists. Use this when you need to change titles, descriptions, or tags across many videos and YouTube Studio makes you click each one. Also covers uploading whole batches of videos from a directory of YAML sidecars.
List and inspect¶
ytstudio videos list # most recent uploads
ytstudio videos list -n 100 -o json # 100 items, JSON
ytstudio videos list --scheduled # only videos scheduled for future publish
ytstudio videos show <video-id> # full metadata for one video
ytstudio videos categories # category ids you can assign to uploads
--scheduled filters to private videos with a future publishAt, which is
the YouTube convention for "scheduled for release". Useful for sanity-checking
a queue you just uploaded.
Update one video¶
ytstudio videos update <video-id> --title "New title"
ytstudio videos update <video-id> --description "New description"
ytstudio videos update <video-id> --tags one,two,three
update targets a single video; pass any combination of --title,
--description, or --tags.
Search-and-replace across the channel¶
videos search-replace walks every video on the active channel, applies
the substitution, and shows what would change. It is dry-run by
default; pass --execute to actually update videos.
ytstudio videos search-replace --search "2024" --replace "2025" --field title
ytstudio videos search-replace \
--search "old@email.com" --replace "new@email.com" --field description \
--execute
ytstudio videos search-replace --search 'season \d' --replace 'season X' \
--field title --regex
Flags:
--search,--replace(required): the substitution.--field(required):titleordescription.--regex: treat--searchas a Python regex.--limit: cap how many matches to act on (default 10).--execute: actually apply the changes.
Quota cost
videos.update costs about 50 quota units per video. A bulk run that
touches 200 videos eats the default daily quota (10 000 units). See
API quota before kicking off large jobs.
Captions and transcripts¶
Read a video's caption tracks, or pull one down as clean plain text. This is the raw material for metadata authoring below and for any workflow that needs the spoken content of a video.
ytstudio videos captions <video-id> # list tracks (language, kind, draft)
ytstudio videos captions <video-id> -o json # same, parseable
ytstudio videos transcript <video-id> # best track as plain text
ytstudio videos transcript <video-id> --lang nl # a specific language
ytstudio videos transcript <video-id> -o json # transcript plus track metadata
captions lists every track with its language, track_kind (standard
for human-authored, ASR for auto-generated, forced), and whether it is a
draft. transcript downloads one track and strips the SRT timing and markup,
leaving one cue per line.
Without --lang, transcript prefers a standard (human) track over ASR
across all languages, which may not be the language you want on a
multi-language channel. List the tracks first and pass the exact code; if the
requested language has no track, the command exits with the available codes.
Auto-generated (ASR) tracks are often restricted by YouTube and may not be
downloadable; transcript then prints a warning and exits non-zero, so fall
back to captions and pick another track.
Quota cost
Listing caption tracks costs about 50 units and downloading a transcript about 200, both well above ordinary 1-unit reads. Author per video rather than scanning a whole channel. See API quota.
Authoring metadata¶
Writing a good title or description is per-video creative work. ytstudio does
not generate copy for you; it supplies the two inputs you need to write it
well, and the existing videos update path applies the result:
- the channel's brand voice (
ytstudio profile brand show) as the source of truth for tone and house style; and - the video's transcript (
ytstudio videos transcript) as the source of truth for what the video actually says.
A single-video loop:
ytstudio profile brand show # house style / tone
ytstudio videos captions <id> -o json # pick a language and track kind
ytstudio videos transcript <id> --lang nl # spoken content
ytstudio videos show <id> -o json # current title/description/tags
ytstudio videos update <id> --title "..." --description "..." # dry-run preview
ytstudio videos update <id> --title "..." --execute # apply
The rules that keep the output honest:
- Brand file for tone, transcript for claims. Never state anything the transcript does not support, and never invent content. If there is no usable transcript, tell the user rather than authoring from nothing.
- Gate the expensive call. Run
captionsfirst and skiptranscriptfor any video without a usable track; a caption list is about 50 units against 200 for a download. - Preview, then execute.
videos updateis dry-run by default; show the user consequential changes before re-running with--execute.
Set the brand voice once per channel; see Brand voice. The agent skill bundled with this repo drives the same loop, including bulk backfill across a set of videos.
Upload a batch from sidecars¶
videos upload takes a video file or a directory, pairs each .mp4/.mov
with a sibling YAML sidecar describing its metadata, validates the lot, and
then uploads them in order. It is dry-run by default; pass --execute
to actually push to YouTube.
ytstudio videos upload ./outbox # dry-run preview
ytstudio videos upload ./outbox --execute # actually upload
ytstudio videos upload ./outbox --execute --max 3 # cap the run (quota budget)
ytstudio videos upload ./outbox/single.mp4 --execute # one specific video
Directory layout¶
outbox/
holiday.mp4
holiday.yaml # required
holiday.jpg # optional thumbnail (.jpg or .png), max 2 MB
travel-vlog.mp4
travel-vlog.yaml
The sidecar must share the video's basename. A thumbnail with the same basename is picked up automatically; near-misses (different extension, wrong case) fail fast with a clear error rather than silently uploading without a thumbnail.
Sidecar (holiday.yaml)¶
title: Holiday recap 2026
description: |
Multi-line description.
privacy: private # private | unlisted | public
publish_at: 2026-06-01T10:00:00+02:00 # optional; forces privacy=private
tags: [travel, vlog]
category_id: "22" # YouTube category id; required. Run `ytstudio videos categories` for the full list.
default_language: nl
default_audio_language: nl
made_for_kids: false
publish_at must be timezone-aware and in the future. If you set it, the
upload pipeline forces privacy=private because that is the only privacy
state YouTube accepts for scheduled releases.
Resumability¶
After each successful upload the sidecar is patched with the resulting
video_id and an uploaded_at timestamp. Re-running videos upload on the
same directory only retries sidecars without a video_id, so partial runs
(quota exhaustion, network drop) are safe to resume.
If the write-back itself fails, the command prints the video_id to stdout
before exiting so you can paste it into the sidecar manually and avoid a
duplicate upload on the next run.
Quota and rate limits¶
videos.insert costs roughly 1600 quota units, so on the default 10k/day
budget you can upload ~6 videos per day. Use --max to cap a run
explicitly. If the API returns quotaExceeded mid-run, the pipeline stops
cleanly, prints how many videos succeeded, and exits non-zero. See
API quota.