Generate Video
Video Generation lets a deployed agent turn a prompt into a short clip, with optional native audio. Agent steps call the typed ctx.sapiom.contentGeneration.video capability; Sapiom supplies the authenticated cloud connection when the deployed agent runs.
Generation takes tens of seconds to minutes rather than the seconds an image takes, so the choice between blocking the step and suspending the workflow matters more here than it does for images.
Choose the generation pattern
Section titled “Choose the generation pattern”| What the agent needs | Recommended operation or input |
|---|---|
| Generate and wait in the current step | contentGeneration.video.create(...) |
| Suspend the workflow while generation finishes | video.launch(...) plus pauseUntilSignal(...) |
| Native audio in the clip | Set audio: true |
| A particular clip length | Set duration in whole seconds |
| A durable private asset | storage: { visibility: "private" } |
| A still image rather than a clip | ctx.sapiom.contentGeneration.images.create or .launch |
create submits the job, polls for it, and resolves once the clip is ready, so an agent awaits it the same way it awaits an image. Use launch when the generation is long enough that holding the step open is a liability, or when the workflow should pause and resume on the finished clip.
Generate and persist a clip
Section titled “Generate and persist a clip”const generation = await ctx.sapiom.contentGeneration.video.create({ prompt: "A calm ocean wave at sunset, slow dolly in", aspectRatio: "16:9", duration: 6, audio: true, storage: { visibility: "private" },});
const video = generation.video;if (!video) { throw new Error("Video generation returned no clip");}
if (video.storageError) { throw new Error(`Video persistence failed: ${video.storageError}`);}
return { resolvedModel: generation.resolvedModel, fileId: video.fileId ?? null, temporaryUrl: video.url,};The result carries resolvedModel and at most one video. Treat the provider-hosted url as temporary; when storage succeeds, retain fileId as the durable reference and mint a fresh URL later with ctx.sapiom.fileStorage.getDownloadUrl(fileId). A storageError means the clip generated but did not persist, so check it rather than assuming a returned clip was stored.
create polls internally. pollIntervalMs (default 5 seconds) and timeoutMs (default 5 minutes) control that loop. Raise timeoutMs before asking a slow model for a long clip rather than letting the step throw partway through a generation that is still running and still billable.
Describe the clip without pinning a provider
Section titled “Describe the clip without pinning a provider”| Field | Accepts |
|---|---|
aspectRatio | 1:1, 16:9, 9:16, 4:3, 3:4 |
resolution | 480p, 720p, 1080p |
duration | Whole seconds |
audio | true to request native audio |
seed | A deterministic seed, where the model exposes one |
negativePrompt | Content to steer away from |
referenceImage | A hosted URL or a Sapiom fileId |
Omit model unless the agent needs a specific Sapiom routing alias; the platform then selects one and echoes the choice as resolvedModel. Models differ in what they support — not every model produces audio, accepts a reference image, or offers every resolution. A field the selected model does not support is rejected before the generation is charged rather than silently ignored, so prefer these neutral fields over provider-specific passthrough values.
Dispatch generation
Section titled “Dispatch generation”video.launch submits the job and returns a handle immediately instead of holding the step open. The handle carries the request ID, the resolved model, a wait() that polls to completion, and the signal metadata pauseUntilSignal needs. The signal is VIDEO_RESULT_SIGNAL and the resumed step receives a VideoResultPayload; import both from @sapiom/tools.
Declare the static pause edge on the launching step. Both signal and resumeStep are required, and the declaration is what allows that step’s run to return a pause directive:
import { defineStep, pauseUntilSignal, terminate, type AgentExecutionContext,} from "@sapiom/agent";import { VIDEO_RESULT_SIGNAL, type VideoResultPayload } from "@sapiom/tools";
const renderClip = defineStep({ name: "render-clip", pause: { signal: VIDEO_RESULT_SIGNAL, resumeStep: "collect-clip" }, async run(input: { prompt: string }, ctx: AgentExecutionContext) { const handle = await ctx.sapiom.contentGeneration.video.launch({ prompt: input.prompt, storage: { visibility: "private" }, });
return pauseUntilSignal(handle, { resumeStep: "collect-clip" }); },});
const collectClip = defineStep({ name: "collect-clip", terminal: true, async run(result: VideoResultPayload, ctx: AgentExecutionContext) { const output = result.outputs[0]; if (!output?.fileId) { throw new Error("Resumed without a stored clip"); }
return terminate({ fileId: output.fileId }); },});The resumed step does not receive the live handle or the video wrapper create returns. It receives a VideoResultPayload: an outputs array plus the generation’s resolvedModel. Import that type from @sapiom/tools and annotate the resumed step’s input with it rather than hand-writing the shape.
A downloadUrl on a resumed output may already have expired — the step can resume long after the URL was minted. Re-fetch from fileId instead of treating a missing or stale URL as a missing asset.
handle.wait() is the alternative to pausing: it polls inline and resolves the same result create would. It accepts timeoutMs and pollMs, defaulting to the input’s timeoutMs and pollIntervalMs. A generation that fails is not currently distinguishable from a slow one on that path — the call surfaces the timeout error either way — so prefer the pause/resume path for long generations, where the completion signal drives the resume instead of a deadline.
Test the behavior locally
Section titled “Test the behavior locally”Local Run replaces video generation with deterministic data and does not generate or store a clip. Override the exact path when a step requires a stored output or branches on a failure:
{ "version": 1, "steps": { "render-clip": { "contentGeneration.video.create": { "video": { "url": "https://fixtures.example/clip.mp4", "contentType": "video/mp4", "fileId": "file-clip-local", "downloadUrl": "https://fixtures.example/clip-download", "downloadUrlExpiresAt": "2099-01-01T00:00:00.000Z" }, "resolvedModel": "stub-model" } } }}contentGeneration.video.launch can use its own override or the shared create result. Supply every field the step consumes, assert the terminal output, and require both unusedStubs and stubWarnings to be empty. A passing fixture does not prove live model availability, motion quality, audio, storage, latency, or usage.
Manage usage and retention
Section titled “Manage usage and retention”Generate the shortest clip the task needs, and request audio and higher resolutions only where the output calls for them. Retain fileId for durable use, mint fresh private download URLs when needed, and use public visibility only for clips intentionally reachable without tenant authentication.
Use the signed-in capability catalog for current availability, limits, and pricing. A video generation costs substantially more than an image, and a step that retries a generation pays for each attempt.
© 2026 Sapiom, Inc.