name: partnership-video-build description: > Fill the ace-web partnership-pitch video template, POST the spec, trigger render, and poll until done. Writes video_spec.yaml + package.yaml. disable-model-invocation: true
Partnership Video Build
Fill the ace-web partnership-pitch video template with the run's produce-phase artifacts — prospect identity, all three narration variants, the picked angle, micro-demo proof clips, and research stat cards — then POST the spec to ace-web, trigger a render, poll until the render completes, and save the output URLs. This is the core produce-phase skill: it converts grounded narrative + proof clips into a rendered partnership pitch video.
Inputs
| Source | Artifact | Used for |
|---|---|---|
| Phase 1 profile | ACE/partnerships/<slug>/prospect.yaml |
Prospect name, logo ref, region, sector, program URL, slug |
partnership-angles |
ACE/partnerships/<slug>/runs/<run-id>/angles.yaml |
All three grounded narration variants; selected_angle from run_state.yaml.phases.angles.products.selected_angle |
partnership-microdemo |
ACE/partnerships/<slug>/runs/<run-id>/micro-demo/provenance.yaml |
Clip entries with is_demo_clip, file_id, caption |
partnership-research |
ACE/partnerships/<slug>/research/deep-research.md |
Problem/impact stat cards with citations |
run_state.yaml |
phases.angles.products.selected_angle |
Which angle is active |
| Env | ACE_WEB_PAT_TOKEN, ACE_WEB_BASE, WORKSPACE_SLUG (default dimagi-team) |
ace-web auth + base URL + workspace |
Products
ACE/partnerships/<slug>/runs/<run-id>/video_spec.yaml— the filled ace-web spec as-POSTed (machine-parsed YAML; viadrive_create_file)ACE/partnerships/<slug>/runs/<run-id>/package.yaml— final output URLs: ace-web program URL, render media URL (viadrive_create_file)ACE/partnerships/<slug>/runs/<run-id>/run_state.yaml— Phase Write-Back:phases.video-build.*
Process
Resolve inputs and check preconditions.
Read
ACE/partnerships/<slug>/runs/<run-id>/run_state.yamlviadrive_read_file. Checkphases.microdemo.verdict— iffailorincomplete, halt with: "partnership-video-build requires a passing partnership-microdemo run. Checkphases.microdemo.verdictin run_state.yaml."Read and capture:
phases.angles.products.selected_angle— the active angle id.ACE_WEB_PAT_TOKENenv var. If unset, halt with: "ACE_WEB_PAT_TOKEN not set; run/ace:ace-web-pat-mintand retry."BASE_URL="${ACE_WEB_BASE:-https://labs.connect.dimagi.com/ace}"(strip trailing slash).WORKSPACE_SLUG="${WORKSPACE_SLUG:-dimagi-team}".
Fetch the template bundle from ace-web.
curl -fsS \ -H "Authorization: Bearer $ACE_WEB_PAT_TOKEN" \ "$BASE_URL/api/w/$WORKSPACE_SLUG/videos/templates/partnership-pitch"On non-200: halt with the status code and body — the
partnership-pitchtemplate (merged via ace-web PR #610) must be deployed before this skill can run. Surface the exact curl error for the operator.From the response, extract:
skeleton_yaml— the spec skeleton with{{placeholder}}tokens.prompt_md— the generation instructions; follow these instructions exactly when filling placeholders (they specify word budgets, output format, and grounding rules).
Read the run artifacts.
Read from Drive via
drive_read_file:ACE/partnerships/<slug>/prospect.yaml→ extractname,slug,logo_asset(ornull),region,sector,program_url(or site URL).ACE/partnerships/<slug>/runs/<run-id>/angles.yaml→ extract all three angle entries; for each angle, extractangle_id,logline,beats(the seven beat texts:hook,cycle,handoff,scene,problem,product,impact).ACE/partnerships/<slug>/runs/<run-id>/micro-demo/provenance.yaml→ extractclips[](each withis_demo_clip,caption,file_id).ACE/partnerships/<slug>/research/deep-research.md→ extract the two strongest cited problem/impact statistics. Each stat must have a citation (author/source name + year or URL). If a stat lacks a citation, use[TBD] <missing citation>rather than including an uncited figure.
Fill the skeleton following the template's
prompt_md.Follow the generation instructions in
prompt_mdexactly, using the artifacts gathered in step 3 as inputs. Key filling rules:- Identity block:
slug= prospect slug;name,region,sector,program_urlfromprospect.yaml;statusfrom deep-research headline (cited);taglinefrom research or[TBD] tagline. - Prospect block:
name,logo_asset,region,sectorfromprospect.yaml. Iflogo_assetis null, omitprospect.logo_assetor use[TBD] logo ref. - Narration variants: embed ALL THREE angles. For each angle, map its seven grounded beat texts to the skeleton's
by_beatkeys (hook,cycle,handoff,scene,problem,product,impact). Respect per-beat word budgets fromprompt_md(±2 words; going long causes mid-word synthesis cuts). Setactive_angletoselected_angle. - Problem block:
big= the cited problem stat from research (e.g. "60%");caption= one sentence describing the problem;source= citation string. Never invent a number — cite directly from deep-research or mark[TBD]. - Impact block: two impact stats from deep-research, each with
big+caption. Both must be cited; uncited stats get[TBD]prefix. - Product beats: populate from
micro-demo/provenance.yaml. For each clip:asset= Drivefile_id,caption= clipcaption, and carryis_demo_clipfrom that clip's provenance entry (do NOT hardcode — a reused library clip may befalse). If no clips exist, write one placeholder beat withasset: "[TBD] clip file_id". - Scene lower third:
"<region> · <prospect name>"or just"<region>"if no prospect name. [TBD]discipline (non-negotiable): any value that cannot be grounded from the available artifacts MUST be written as[TBD] <what is missing>. Never invent a stat, name, or fact. After filling, scan the completed spec and count[TBD]tokens — if any remain (permitted for unresolvable gaps), report them in the write-back and QA checks. The skill must NOT proceed past inline QA if a[TBD]appears in a narration beat — narration is the prospect-facing surface; unresolved narration text is a hard block.
- Identity block:
Validate the filled spec before POST.
Confirm:
- No
{{placeholder tokens remain (all were replaced by values or[TBD]). - No
[TBD]in anyby_beatnarration string across all three variants (these are the prospect-facing text; any unresolved narration beats must be resolved before POSTing). - The
active_anglevalue matches one of the threeangle_idvalues invariants.
If either of the first two checks fails, halt and report: "Spec validation failed before POST — [list issues]. Resolve these gaps in the source artifacts and re-run."
- No
POST the program to ace-web.
curl -fsS -X POST \ -H "Authorization: Bearer $ACE_WEB_PAT_TOKEN" \ -H "Content-Type: application/json" \ "$BASE_URL/api/w/$WORKSPACE_SLUG/videos/programs" \ -d "$(jq -nc --arg slug "$PROGRAM_SLUG" --arg spec "$SPEC_YAML" \ '{slug: $slug, spec_yaml: $spec}')"Expect 2xx. Capture the response's
run_id(typically"run-001"),program_slug, andspec_path.On 409 (slug already exists): halt with: "Video program
<slug>already exists in workspace<ws>. Either pick a different slug or delete the existing program in the ace-web UI before re-running."On 400: surface the
detailfield from the response — the server rejected the spec structure; revisit the placeholder fill in step 4.Trigger the render.
curl -fsS -X POST \ -H "Authorization: Bearer $ACE_WEB_PAT_TOKEN" \ -H "Content-Type: application/json" \ "$BASE_URL/api/w/$WORKSPACE_SLUG/videos/programs/$PROGRAM_SLUG/runs/$RUN_ID/build" \ -d '{"mode": "render"}'Expect 2xx (
ok: true). If the response body hastriggered: false, log a WARN — the server did not queue the render — and continue to step 8 (the status poll will catchappears_failed).Poll render status until complete.
Poll up to 30 times with a 10-second interval (5 minutes total — the typical render is 60–90s):
curl -fsS \ -H "Authorization: Bearer $ACE_WEB_PAT_TOKEN" \ "$BASE_URL/api/w/$WORKSPACE_SLUG/videos/programs/$PROGRAM_SLUG/runs/$RUN_ID/render-status"On each poll: if
busy: false, the render is done — proceed. Ifappears_failed: true(present in any poll response), stop polling and recordrender_failed: true.After 30 polls without
busy: false, recordrender_failed: true(timeout).Construct the output URLs.
- Editable program URL:
$BASE_URL/w/$WORKSPACE_SLUG/videos/$PROGRAM_SLUG - Render media base:
$BASE_URL/api/w/$WORKSPACE_SLUG/videos/programs/$PROGRAM_SLUG/runs/$RUN_ID/media/ - The primary output file is conventionally
output.mp4(or whatever the server returns as the canonical render output — checkrender-statusfor aoutput_filesfield if present; fall back tooutput.mp4).
- Editable program URL:
Save
video_spec.yamlandpackage.yamlto Drive.Both are machine-parsed YAML files. Use
drive_create_file(it uploads body astext/plain— NOTdrive_create_doc_from_markdown, which makes a Google Doc that mangles YAML on read-back). Parent folder = the run folder (runs/<run-id>/).video_spec.yaml— the filled spec as-POSTed:prospect_slug: <slug> run_id: <run-id> generated_at: <ISO timestamp> program_slug: <program_slug> ace_web_run_id: <run_id from POST response> spec_yaml: | <the full filled spec, verbatim>package.yaml— final delivery URLs:prospect_slug: <slug> run_id: <run-id> generated_at: <ISO timestamp> video: program_url: <editable ace-web URL> media_url: <render media URL> run_id: <ace_web_run_id> render_status: completed | failed | timeout appears_failed: <true|false>Inline QA (binary — run before writing the phase write-back).
Verify all of the following. Record which checks pass and which fail — the write-back is ALWAYS written regardless of outcome:
spec_posted: POST to ace-web returned 2xx.render_completed:busy: falsereceived before timeout ANDappears_failedis not true.package_has_urls:package.yamlcontains non-nullvideo.program_urlandvideo.media_url.no_tbd_in_narration: no[TBD]tokens appear in anyby_beatnarration string in the posted spec (this re-checks the pre-POST guard; if a[TBD]slipped through the step 5 check, catch it here).active_angle_valid:active_anglein the posted spec matchesselected_anglefromrun_state.yaml.
Write the Phase Write-Back to
run_state.yaml(always — both pass and fail paths).Write
phases.video-build.*toACE/partnerships/<slug>/runs/<run-id>/run_state.yamlviaupdate_yaml_filewithmerge: 'deep'(a partial nested patch ofphases.<phase>requiresdeep—two-levelsilently drops sibling keys; see CLAUDE.md § Gotchas):QA pass path (
verdict: pass):phases: video-build: status: done verdict: pass completed_at: <ISO timestamp> summary_artifact: video_spec.yaml steps: preconditions_checked: done template_fetched: done artifacts_read: done spec_filled: done spec_validated: done spec_posted: done render_triggered: done render_polled: done artifacts_saved: done inline_qa: done write_back: done products: program_slug: <program_slug> ace_web_run_id: <run_id> program_url: <editable ace-web URL> media_url: <render media URL> video_spec_file_id: <Drive file_id> package_file_id: <Drive file_id> tbd_count: <number of [TBD] tokens in the posted spec outside narration> render_failed: falseQA fail path (
verdict: fail): write the write-back first, then halt with the failed check names surfaced.phases: video-build: status: incomplete verdict: fail completed_at: <ISO timestamp> summary_artifact: video_spec.yaml steps: preconditions_checked: done template_fetched: done artifacts_read: done spec_filled: done spec_validated: done spec_posted: <done|fail> render_triggered: <done|fail> render_polled: <done|fail> artifacts_saved: done inline_qa: fail write_back: done products: program_slug: <program_slug or null> ace_web_run_id: <run_id or null> program_url: <URL or null> media_url: <URL or null> video_spec_file_id: <file_id or null> package_file_id: <file_id or null> tbd_count: <count> render_failed: <true|false> qa_failures: - <failed-check-id>After writing the write-back on the fail path, halt with an actionable operator error naming the failed checks.
Environment
| Variable | Default | Notes |
|---|---|---|
ACE_WEB_PAT_TOKEN |
(required) | Per-human Bearer token; mint via /ace:ace-web-pat-mint |
ACE_WEB_BASE |
https://labs.connect.dimagi.com/ace |
ace-web base URL (strip trailing slash) |
WORKSPACE_SLUG |
dimagi-team |
ace-web workspace slug |
MCP Tools Used
- Google Drive:
drive_read_file,drive_create_file,update_yaml_file - ace-web video API: all HTTP calls via Bash/curl with
ACE_WEB_PAT_TOKEN— NOT MCP atoms. Endpoints used:GET /api/w/<ws>/videos/templates/partnership-pitch— fetch template bundlePOST /api/w/<ws>/videos/programs— create program with filled specPOST /api/w/<ws>/videos/programs/<slug>/runs/<run_id>/build— trigger renderGET /api/w/<ws>/videos/programs/<slug>/runs/<run_id>/render-status— poll status
Note: drive_create_folder is NOT called here — video_spec.yaml and package.yaml live at the run folder root (not in a subfolder), so no subfolder creation is needed. Both files use drive_create_file with the run folder as parentFolderId.
Mode Behavior
- Auto: Run all steps, fill spec, POST, render, poll, save artifacts, write phase write-back, stop. No interactive prompt.
- Review: Same as Auto. The produce-phase operator review gate is at the orchestrator level (operator reviews the video before publish), not inside this skill.
Dry-Run Behavior
When --dry-run is active:
- Read inputs normally (read-only operations are safe in dry-run).
- Fetch the template bundle normally (read-only HTTP GET).
- Fill the skeleton normally (in-memory LLM work).
- Skip the
POST /videos/programscall, thePOST .../buildcall, and the render poll. - Write
video_spec.yamlto Drive withace_web_run_id: dry-runand the filled spec (so the operator can review it before committing to render). - Write
package.yamlwithrender_status: dry-run,program_url: null,media_url: null. - State tracks as
dry-run-success.
Change Log
| Date | Change | Author |
|---|---|---|
| 2026-06-06 | Initial version. Produce-phase video-spec fill → POST → render → poll. Inline QA: spec_posted, render_completed, package_has_urls, no_tbd_in_narration, active_angle_valid. Hard block on [TBD] in narration pre-POST. merge: deep write-back. Phase folder: 8-video-build/. | ACE team |