When you approve a package's youtube_title_set asset, the dispatch
worker routes it by the brand's youtube_dispatch_target. There are
three settings: Direct YouTube Data API v3 (uploads straight from this Mac — built
and recommended), Zernio (cloud publishing — the only path for Shorts in v1), and
manual (the safe default — you paste fields into YouTube Studio yourself). This page
is the current, code-accurate map of all three.
The pickTarget() switch in workers/kinds/dispatch.ts decides. Only youtube_title_set triggers a long-form upload; its sibling youtube_description / youtube_chapters / youtube_tags assets ride along inside that one call (or are pasted, on the manual path).
Default youtube_dispatch_target = 'manual'. Direct fires only when the brand opted in and an OAuth refresh token is present — otherwise it falls back to manual rather than failing silently.
All three end with your video published. They differ in who holds the auth, whether a public URL is needed, and whether they cover Shorts and other networks.
| ① Direct YouTube API recommended · built | ② Zernio built · Shorts | ③ Manual paste default | |
|---|---|---|---|
| how the file reaches YT | The dispatch worker streams original.mp4 straight to YouTube's resumable upload endpoint |
Zernio fetches the rendered clip from your signed media URL, then publishes it to its connected networks | You upload the file by hand at studio.youtube.com |
| public URL needed | no — Mac → Google, direct HTTPS | yes — signed /media/* via CLOUDFLARE_TUNNEL_HOSTNAME + MEDIA_URL_SECRET |
no |
| who holds the OAuth | You — per brand, refresh token encrypted on brands.youtube_oauth |
Zernio holds the platform connections; you hold a Zernio API key | You, in the browser — nothing stored |
| YT API quota lives on | Your Google Cloud project (≈10k units/day = ~6 uploads/day) | Zernio's app, plus a §9.6 cap of 20 posts/account/day | n/a — browser upload |
| covers long-form video | yes — its whole job | technically — but Direct is the long-form path; Zernio shines for Shorts | yes |
| covers YT Shorts | deferred to v2 | yes — rendered_short_clip → TT + IG + YT Shorts in one call |
manual upload |
| covers LinkedIn / X / TikTok | no | yes — same pipeline routes those too | no |
| privacy / scheduling | Full: public / unlisted / private (default) / schedule via per-package picker → videos.insert |
privacy='schedule' + publish_at → Zernio scheduledFor |
You pick it in YouTube Studio |
| operator setup | ~5 min — Google Cloud OAuth client, paste GOOGLE_OAUTH_CLIENT_ID/SECRET in /settings, click Connect YouTube on the brand |
~10 min — set ZERNIO_API_KEY, connect networks in Zernio, paste acc_… on the brand, stand up a signed-media tunnel |
none |
| good for | Local-first, single-creator setups where YouTube long-form is the primary destination | Anyone cross-posting Shorts + social, who already lives in Zernio | One-offs, smoke tests, or when you simply want control |
ChannelHelm uploads the file straight to YouTube's resumable endpoint. No tunnel, no third party — just OAuth between your brand and Google. Already implemented in workers/integrations/youtube.ts.
Honors the per-package privacy picker. With schedule, the worker normalizes to private + publishAt — YouTube auto-flips public at the scheduled time.
youtube.upload + youtube), then create one OAuth client of type Web application with redirect URI http://localhost:3000/api/youtube/oauth/callback.GOOGLE_OAUTH_CLIENT_ID + GOOGLE_OAUTH_CLIENT_SECRET in /settings. One client serves every brand./brands/[id], click Connect YouTube → Google consent → back with the refresh token saved (encrypted) on the brand row. Set the brand's dispatch target to youtube_direct.youtube_title_set → dispatch worker uploads → the red ▶ youtu.be/… chip populates.brands.youtube_oauth jsonb (encrypted refresh_token, channel_id/title, scope, connected_at) + brands.youtube_dispatch_target text default 'manual'.workers/integrations/youtube.ts — youtubeAuthUrl(), on-demand token refresh, uploadVideo() (resumable, streams from disk), thumbnails.set of the picked concept (an AI-generated image, or a captured frame on the fallback path)./api/youtube/oauth/start · /callback · /disconnect.dispatch.ts youtube_direct branch: bundles siblings, honors privacy/schedule, mirrors the URL onto packages.intelligence.published.youtube, flips youtube_description/chapters/tags to dispatched in the same transaction.setYoutubePublishOptions(); per-brand routing via setYoutubeDispatchTarget().# Google Cloud Console — one-time, free tier sufficient 1. New project → "ChannelHelm" 2. APIs & Services → Library → enable YouTube Data API v3 3. OAuth consent → External → scopes: youtube.upload, youtube 4. Credentials → OAuth client ID → Web application Authorized redirect URI: http://localhost:3000/api/youtube/oauth/callback 5. Copy Client ID + Client secret → paste in /settings GOOGLE_OAUTH_CLIENT_ID · GOOGLE_OAUTH_CLIENT_SECRET
PROVIDER_SECRET_KEY would break the saved refresh token (same caveat as LLM provider keys)Zernio is the external publishing API. It routes social posts and rendered clips, and one post can hit TikTok + Instagram + YouTube Shorts at once. In v1, this is the only path for Shorts.
Shorts (rendered_short_clip) and social (linkedin_post / x_post / x_thread) all flow this way. A §9.6 cap of 20 successful posts/account/24 h requeues to the next UTC day rather than failing.
ZERNIO_API_KEY in /settings and connect your networks (TikTok / IG / YouTube) in the Zernio dashboard.acc_… id into the brand's zernio_accounts map at /brands/[id].CLOUDFLARE_TUNNEL_HOSTNAME + MEDIA_URL_SECRET + MEDIA_REQUIRE_SIGNATURE=1.short_clip_plan → clip_render), then approve each rendered_short_clip → it dispatches via Zernio.workers/integrations/zernio.ts — createPost(), resolveZernioPlatforms(); thin typed fetch fallback in zernio_http.ts.dispatch.ts zernio branch: per-clip platform toggles (publish_options.platforms), per-clip scheduling (privacy='schedule' + publish_at → scheduledFor), signed media URLs for clips./api/webhooks/zernio, idempotent on (source, source_event_id).RequeueLater.createPost fans out to many networks at onceThe default and always-available path. ChannelHelm gives you every field; you upload the MP4 and paste them into YouTube Studio. The dispatch is still recorded for auditability — it just makes no API call.
No YouTube webhook exists for manual uploads, so the asset settles at dispatched rather than published. Switch the brand to youtube_direct to get true published tracking.
dispatched, never publishedOne Google Cloud OAuth client serves every brand. Each brand connects once; the refresh token lands encrypted on its row. Re-running just overwrites it.
The per-package picker writes packages.intelligence.publish_options.youtube. The Direct branch reads it and normalizes. The default is private — nothing goes public by accident.
YouTube is one of three downstream systems. This is the complete pickTarget() map: each asset type resolves to exactly one of DojoClaw, Zernio, YouTube Direct, or manual.
* youtube_title_set resolves to YouTube Direct only when the brand has youtube_dispatch_target='youtube_direct' AND a refresh token; otherwise it falls back to manual. The other youtube_* assets are pasted manually (or ride along inside the Direct upload). *_plan assets are never dispatched — they're rendered into rendered_* first.
Pick by what you actually publish, not by what sounds fancy.
youtube_direct, connect once.
createPost hits multiple networks; per-clip toggles and scheduling are already wired through the Shorts editor. Needs a signed-media tunnel.
Direct is built. Paste GOOGLE_OAUTH_CLIENT_ID/SECRET in /settings, click Connect YouTube on the brand, set the dispatch target to youtube_direct, and the next approval uploads itself.