video-hosting-publish
Code drafts. A human decides. Code publishes. This skill owns the publish step. It is paired with
/video-hosting-draft. It does not generate anything. It does not interpret the editor's intent. It reads the cell value the editor left, hands it to the video hosting API, and records what came back.
What this skill does (one paragraph)
For each row in the editorial Google Sheet where the editor has ticked the approval checkbox and which has not already been published, this skill takes the editor's final values (the final_title, final_description, final_thumbnail_url cells — not the draft variants the agent generated), uploads to the video hosting API, and writes the resulting clip id and a timestamp back to the row so the same row will be skipped on every subsequent run. This is the central HITL guarantee: the publish step trusts the human's final values, never the model's drafts.
Inputs
- The editorial Google Sheet (id in
config.yaml, same id as/video-hosting-draft). - A video hosting API token, read from an env var (
VIDEO_HOSTING_TOKEN). Never read from the repo, never logged. - The original
videos/folder — to upload the actual.mp4byte stream referenced bysource_path.
How "approved" is detected — matches the draft skill
The approval mechanism in this skill must match the mechanism declared by /video-hosting-draft. They share the same sheet contract:
- Column:
approved. - Type: Google Sheets checkbox (data validation: checkbox), value
TRUE/FALSE. - The skill reads the column and treats only rows with
approved = TRUEas eligible. Any other value, blank, or "kind of" — ignored.
If /video-hosting-draft ever changes the approval mechanism (e.g. from a checkbox to a status column with an enum), this skill must change with it. They are designed and shipped as a pair.
How the final values are picked — also matches the draft skill
The sheet has two parallel sets of columns: the agent's drafts (title_1..3, description_1..3, thumbnail_1..3_url) and the editor's finals (final_title, final_description, final_thumbnail_url).
This skill only ever reads the final_* columns, in order to honor the editor's decision exactly. Specifically:
final_title→ published title. Not the highest-weightedtitle_N, not a fallback totitle_1, not a "merged best" of the three.final_description→ published description.final_thumbnail_url→ the Drive link to the PNG to upload as the cover. The editor may have picked one of the three drafts, or edited one of them, or uploaded their own — this skill doesn't care; it just reads the cell.
This is the central HITL guarantee — the human's final value is what ships. If the editor cleared a final cell, the row is treated as incomplete and skipped with a warning. The skill never falls back to a draft when a final is empty.
(This mirrors /video-hosting-draft's contract: draft writes to *_1..3, editor writes to final_* and the approved checkbox, publish reads final_*.)
Skip already-published rows (idempotency)
The skill is safe to re-run on every cron tick. The idempotency mechanism is:
- Read the sheet (one API call).
- Filter rows:
approved == TRUEandpublished_clip_idis empty. - Only those rows enter the publish loop. Any row that already has a
published_clip_idis skipped without an API call, without a state check, without anything that could double-publish.
So the universe is: "approved, not yet published". The same row appears in this universe at most once.
Marking the row after a successful upload
After the video hosting API returns success, the skill writes back two cells in the same row, in a single batch_update:
published_clip_id← the clip id returned by the hosting API (the marker that proves the row was published; also what the gallery URL is built from).published_at← ISO 8601 UTC timestamp of the successful response.
These two cells are the persistent marker. They are how the next run knows to skip the row (see Skip section above). They are also how the editor sees, at a glance, what has shipped.
If the API call succeeded but the writeback failed, the skill retries the writeback with exponential backoff. If the writeback ultimately fails, the skill logs an error with the clip id so a human can reconcile — and on the next run the row will appear as "approved but not marked", which the skill detects and handles by querying the hosting API for "did clip with this title from this source already get uploaded?" before re-uploading, to avoid the double-publish case the marker normally prevents.
End-to-end run
- Pull the sheet. Filter to
approved == TRUE AND published_clip_id IS EMPTY. - For each such row, in order:
a. Read
final_title,final_description,final_thumbnail_url,source_path. b. Validate all four are non-empty. If any is empty, log a warning, skip the row, continue. c. Download the thumbnail PNG fromfinal_thumbnail_url. d.POSTto the video hosting API (/api/clips) with the.mp4, the PNG, the title, the description. e. On 2xx: capture the returnedclip_id. Writepublished_clip_idandpublished_atback to that row. f. On non-2xx: log with the row id, do not write back, do not retry the upload in the same run (avoids accidental duplication on hosting). The next run will pick it up again. - Print a one-line summary: published N, skipped M (already published), warned K (incomplete final values).
What this skill never does
- Never generates or rewrites a title, description, or thumbnail. That is
/video-hosting-draft. - Never reads
title_1..3,description_1..3, orthumbnail_1..3_url. The drafts are only there for the editor's eyes. - Never publishes a row whose
approvedcell is anything butTRUE. - Never publishes a row that already has a
published_clip_id. - Never overwrites a row the editor edited after publish — the marker columns are append-only from the publish skill's side.