Skip to content
Go To Dashboard

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.

What the agent needsRecommended operation or input
Generate and wait in the current stepcontentGeneration.video.create(...)
Suspend the workflow while generation finishesvideo.launch(...) plus pauseUntilSignal(...)
Native audio in the clipSet audio: true
A particular clip lengthSet duration in whole seconds
A durable private assetstorage: { visibility: "private" }
A still image rather than a clipctx.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.

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”
FieldAccepts
aspectRatio1:1, 16:9, 9:16, 4:3, 3:4
resolution480p, 720p, 1080p
durationWhole seconds
audiotrue to request native audio
seedA deterministic seed, where the model exposes one
negativePromptContent to steer away from
referenceImageA 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.

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.

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.

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.