GitHub Actions job that renders a 9:16 clip on Sume as an artifact
A workflow file that replaces a Sora render step: submit to Sume, poll with a deadline, and upload the mp4 as a build artifact. The key stays in a secret.

To render a video in CI, store SUME_API_KEY as a repository secret, submit to POST /v1/videos with an idempotency key built from the commit, poll until completed, and upload the downloaded mp4 as an artifact. A release job that called the OpenAI Sora API stopped working when the API ended on 2026-09-24 (Magic Hour tracker, read 2026-10-06), and this is the minimal replacement step.
The workflow below is one job with one shell step. It uses only curl and jq, which a hosted Ubuntu runner provides, and it never prints the key.
What each part does
| Part | What it does | Docs basis |
|---|---|---|
| secrets.SUME_API_KEY | Bearer key for the API | Authorization: Bearer header |
| Idempotency-Key from the commit SHA | A rerun of the job returns the same video job | Replay returns the original job |
| Poll every 30 s with a deadline | Waits without a long HTTP call | 30 s poll suggested; sync caps at 30 s |
| unsigned_urls[0] download | Fetches the finished mp4 | Content URL on a completed job |
| upload-artifact | Keeps the file with the run | Your storage, not the vendor's |
The workflow
Put this in .github/workflows/render.yml. It renders a 5-second vertical clip on Omni at 720p, which costs $0.63 per run.
name: render-clip
on: workflow_dispatch
jobs:
render:
runs-on: ubuntu-latest
steps:
- name: Render
env:
SUME_API_KEY: ${{ secrets.SUME_API_KEY }}
run: |
set -euo pipefail
H="Authorization: Bearer $SUME_API_KEY"
POLL=$(curl -sf -X POST https://api.sume.com/v1/videos -H "$H" \
-H "Idempotency-Key: ci-${GITHUB_SHA}" -H 'Content-Type: application/json' \
-d '{"model":"gemini-omni-flash-1.1","prompt":"A paper plane glides over a desk, soft light","aspect_ratio":"9:16","resolution":"720p","duration":5}' \
| jq -r .polling_url)
for i in $(seq 1 30); do
sleep 30
S=$(curl -sf "$POLL" -H "$H")
case $(echo "$S" | jq -r .status) in
completed) curl -sf -H "$H" -o clip.mp4 "$(echo "$S" | jq -r '.unsigned_urls[0]')"; exit 0;;
failed|cancelled) echo "$S" | jq .error; exit 1;;
esac
done
echo timeout; exit 1
- uses: actions/upload-artifact@v4
with: {name: clip, path: clip.mp4}Why it is shaped this way
- The idempotency key uses the commit SHA, so a re-run of the same commit returns the same job and does not bill twice. A manual change of the prompt in the file needs a new commit, which gives a new key.
- The loop has a fixed ceiling of 30 polls, so a stuck job fails the run in 15 minutes instead of holding a runner.
- The download sends the bearer header in case the content URL needs it; the docs show both the open and authenticated forms.
- On a failed job the step prints the
errorfield and exits non-zero, so the run is red.
Two limits to respect. Hosted runners are not a place for a hundred renders; use a queue and a callback for volume, as in the exit and feature-flag post for how to switch a feature over. And artifacts expire on GitHub's schedule, so copy anything you keep long term to your own storage, as the file-lifetime post explains for Sume results.
Hardening the job
The workflow above is the shortest version that works. Before it runs on every push, add the guards a paid step needs. Trigger it on a tag or on a manual dispatch instead of every commit, because every new commit is a new idempotency key and so a new paid job. A pull request from a fork does not receive repository secrets, so the step should be skipped there rather than fail with an empty key.
Also decide what a failed render does to the pipeline. For a marketing asset, a red run that blocks a release is probably wrong; mark the job as allowed to fail, and have the next step use the previous artifact. For a release video that must exist, keep it red.
Add a final step that writes the job id and cost to the run summary. A reviewer who opens the run then sees which model rendered the clip, what it cost and where the artifact is, without opening the log, and a finance question about CI spend can be answered from run summaries alone.
- Run on
workflow_dispatchor a version tag, not on every push. - Set a job-level
timeout-minutesslightly above the poll ceiling, so a hung runner is cut off even if the script misbehaves. - Never echo the key. If you print the request, print the body only, and let the
Authorizationheader stay inside thecurlcall. - Pin the model id and settings in the workflow file, so a render is reproducible from the commit alone.
- Upload the artifact even when the clip is short; a retention of a few days is enough for review.
Sources
Related posts
More in Integrations
- stt_create dry_run and max_spend_usd: cap an MCP transcription run
On Sume's hosted MCP, dry_run previews admission and cost without submitting, and max_spend_usd caps a paid stt_create. Add an idempotency_key to every write.
- Viber bot video message: 26 MB, 180 s, .mp4 URL, a Sume clip
Viber's bot API wants an MP4 URL ending in .mp4, 26 MB max, 180 s max, and a JPEG thumbnail. A 60 second Sume avatar clip fits. The checks to run first.
- WhatsApp Business MCP is dev-only: keep Sume generation server-side
WhatsApp Business tools MCP was announced as dev and test only. Generate with Sume on your server and gate the MCP transport by environment. Tested code.
- How to add an MCP server to ChatGPT with developer mode
Turn on ChatGPT developer mode, create an app for the server's URL, and sign in with OAuth. The steps, with Sume's hosted MCP server as the example.
Written by Sume