Pipelines

Process video files through versioned graphs of analysis, review, redaction and publishing steps.

Editframe enables pipelines for each organization after you tell us about your footage. Until then, every pipeline route returns 404.

A pipeline is a named, versioned graph of steps. A run processes one video file through one version of a pipeline. Each step starts when the steps it needs have finished, so independent steps run in parallel.

Editframe runs the built-in steps: probe, transcription, frame sampling, scene detection, redaction and publishing. An endpoint step sends the media and earlier results to your own service. Filter and review steps decide whether the rest of a branch runs.

Every step writes its output to a namespace on the file. Later steps, filters and your own code read outputs by namespace. See Pipeline runs to monitor, cancel, retry and review runs.

Quick start

The pipeline SDK is the @editframe/api/pipelines entry point of @editframe/api. It is experimental and can change without a major version.

js
import {
  Client,
  createPipeline,
  createPipelineRun,
  getPipelineRun,
  isPipelineRunTerminal,
} from "@editframe/api/pipelines";

const client = new Client(process.env.EDITFRAME_API_KEY);

const pipeline = await createPipeline(client, {
  name: "speech-check",
  definition: {
    steps: {
      probe: { type: "probe" },
      transcript: { type: "transcribe", needs: ["probe"] },
      has_speech: {
        type: "filter",
        needs: ["transcript"],
        when: { path: "transcript.metadata.word_count", op: "gt", value: 0 },
      },
      publish: { type: "publish", needs: ["has_speech"] },
    },
  },
});

const { run } = await createPipelineRun(
  client,
  { pipeline_id: pipeline.id, file_id: "your-video-file-id" },
  { idempotencyKey: "speech-check:your-video-file-id" },
);

let current = run;
while (!isPipelineRunTerminal(current)) {
  await new Promise((resolve) => setTimeout(resolve, 5000));
  current = await getPipelineRun(client, run.id);
}
console.log(current.status); // "succeeded", "failed", "filtered" or "cancelled"

The same flow with the CLI, which reads the API key from EDITFRAME_TOKEN:

bash
export EDITFRAME_TOKEN="$EDITFRAME_API_KEY"
npx editframe pipelines create speech-check --definition speech-check.json
npx editframe pipelines runs create PIPELINE_ID FILE_ID --watch

--watch polls the run until it finishes and exits with status 1 when the run fails or is cancelled. See CLI: pipelines for every command.

Definitions

A definition is a JSON object with one field, steps. Each key of steps is a step key: 1–63 characters of lowercase letters, digits and underscores, starting with a letter. A definition has 1–64 steps.

Unknown fields cause a validation error. Editframe stores the normalized definition: it records every default, sorts needs, kinds and formats, and computes a digest (sha256: followed by the SHA-256 of the canonical JSON). Equivalent definitions have the same digest.

Step types

Notes on the built-in steps:

  • probe is final: it runs once the whole file is available.
  • transcribe reads the first audio track. When an output would exceed the output limits, it keeps segments in time order and includes words only if all of them fit. truncated reports that annotations were left out.
  • sample_frames takes a frame at each multiple of every_ms. Endpoint steps that need it receive the frame URLs.
  • redact_render blurs every annotation of the selected kinds that has keyframes, and mutes the audio of every one without keyframes. It reads the annotations of every step it needs, directly or indirectly, with review corrections applied. It creates an ordinary render in your organization, which uses render minutes like any other render. Editframe then adds the rendered MP4 to your organization as a new file, named after the input with a -redacted.mp4 suffix, and the step succeeds when that file is ready. output_file_id is its ID. Like an upload, it sends file.processing and then file.ready or file.failed webhooks, and it expires when the input file does. When nothing matches, the step succeeds without rendering, and the output's status is skipped. A failed render or redacted file fails the step without an automatic retry; retrying the run renders again.
  • With captions: true, redact_render burns in captions from the transcript of the one transcribe step it needs, directly or indirectly; needing none fails validation with not_upstream at with.captions, and more than one with not_allowed. Each transcript segment is one caption, and a muted word shows as ▇▇▇. With title, it shows title.text, trimmed of white space, at the top of the video until duration_ms, fading out over the last half second. A step with captions or a title always renders, even when nothing matches.
  • publish returns playback URLs for the run's input file. After a redact_render step it needs, directly or indirectly, it plays the redacted file instead, or the input file when the redaction was skipped. file_id is the file it plays. A publish step can need only one redact_render step, directly or indirectly; more fail validation with not_allowed at its needs. Requests to the URLs need an API key, a session or a signed URL token.

Segment steps

A step with granularity { "mode": "segment", "window_ms": 60000, "overlap_ms": 2000 } processes the file in windows instead of all at once. window_ms is 1000–3,600,000. overlap_ms is 0 to half of window_ms, default 0. Only transcribe, sample_frames, detect_scenes and endpoint steps can use segment mode.

Windows start on fragment boundaries, so a window can be longer than window_ms and an overlap can be longer than overlap_ms. The last window ends at the end of the file. Each window has its own output, and its annotations must be inside the window.

Segment steps can start before an upload finishes. With a streamed upload, a segment step processes each window as soon as the media for it has arrived. This is true only when every step it needs is also a segment step: a whole-file step it needs must finish first, and whole-file steps wait for the complete file. A URL source is complete when it is created, so all of its windows are ready at once.

Branching with filters

A filter step evaluates its when predicate against the outputs of earlier steps. When the predicate is true, the filter succeeds. When it is false, the filter is skipped, and so is every step that needs it, directly or indirectly.

json
{
  "type": "filter",
  "needs": ["detector"],
  "when": {
    "any": [
      { "path": "detector.annotations", "op": "non_empty" },
      { "path": "detector.metadata.flagged", "op": "eq", "value": true }
    ]
  }
}

A predicate is a leaf or a combination of predicates:

  • { "all": [...] } is true when every child is true. { "any": [...] } is true when one child is true. Each takes 1–16 children.
  • { "not": predicate } inverts its child.
  • A leaf is { "path", "op", "value" }. Predicates nest at most 4 levels deep.

A path is <namespace>.metadata.<field>... or <namespace>.annotations..., with at most 16 segments. A numeric segment selects an array item, for example detector.annotations.0.kind. A filter can read only the namespaces of steps it needs, directly or indirectly.

Every leaf is false when nothing is at path, including ne. Strings in value have at most 1024 characters.

A filter reads a merged view of each segment step: the annotations of every window, and each metadata field from the last window that sets it.

To branch, give two filters complementary predicates. The branch that is not taken is skipped, and the run ends succeeded when the taken branch reaches its final step. A step that needs steps from both branches is always skipped. See Run statuses.

Review steps

A review step pauses its branch until a person approves or rejects the results of the steps before it.

json
{
  "type": "review",
  "needs": ["has_faces"],
  "with": {
    "instructions": "Check every face box. Remove false detections.",
    "expires_after_ms": 86400000,
    "on_expiry": "reject"
  }
}

When the review step starts, Editframe opens a review task, and the step waits in waiting_review. The decision changes the run:

  • Approve: the step succeeds. The approval can include corrected annotations, which replace the original annotations of their kinds for every later step.
  • Reject: the step is skipped, so the steps after it are skipped. The run ends filtered unless another branch reaches a final step.
  • Expiry: when nobody decides within expires_after_ms, on_expiry applies. approve succeeds the step with the decision expired. reject skips the step. fail fails the step and the run, and a run retry opens a new review task.

Decide reviews through the API, the SDK, the CLI or the dashboard. See Reviews.

Endpoint steps

An endpoint step sends a signed HTTP request to your service and uses the response as the step's output. First register the endpoint:

js
import { createPipelineEndpoint } from "@editframe/api/pipelines";

const endpoint = await createPipelineEndpoint(client, {
  url: "https://models.example.com/editframe/faces",
  headers: { Authorization: `Bearer ${process.env.MODEL_TOKEN}` },
  timeout_ms: 30000,
  callback_timeout_ms: 3600000,
  max_concurrency: 16,
});

// Store this secret now. Editframe returns it only once.
console.log(endpoint.signing_secret); // "whsec_..."

You cannot set headers that Editframe sets itself: host, connection, keep-alive, upgrade, te, trailer, transfer-encoding, expect, content-length, content-type, content-encoding, user-agent, or any header that starts with proxy- or webhook-.

You can change the timeout_ms, callback_timeout_ms and max_concurrency of an endpoint. Send only the limits to change. Requests sent after the change use the new limits; requests already sent keep their timeouts:

js
import { updatePipelineEndpoint } from "@editframe/api/pipelines";

await updatePipelineEndpoint(client, endpoint.id, { max_concurrency: 4, timeout_ms: 45000 });

The ranges are the same as for a new endpoint. A body with no limit, an unknown field or a value out of range fails with 422 invalid_request. An archived endpoint keeps its limits: the change fails with 409 endpoint_archived. Editframe records each change, with the API key or user that made it, in the endpoint's change history in the dashboard. A change to values that the endpoint already has records nothing.

You cannot change the URL, the headers or the signing secret. To change them, create an endpoint, create a pipeline version that uses it, and then archive the old endpoint. A new version cannot name an archived endpoint, and a run whose version names an archived endpoint fails with endpoint_not_found.

Then reference the endpoint from a step:

json
{
  "type": "endpoint",
  "needs": ["frames", "transcript"],
  "granularity": { "mode": "segment", "window_ms": 60000 },
  "with": { "endpoint_id": "6f1c2a9e-3b4d-4e5f-8a7b-1c2d3e4f5a6b", "max_concurrency": 16 }
}

Request

Editframe sends POST with a JSON body and the User-Agent Editframe-Pipelines/1:

json
{
  "pipeline_id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
  "pipeline_version": 12,
  "pipeline_run_id": "3d0f6c1a-5b2e-4c7d-9f8a-1b2c3d4e5f60",
  "step_run_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
  "step_key": "detector",
  "namespace": "detector",
  "attempt": 1,
  "idempotency_key": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f:1:14",
  "external_id": "truck-1182/2026-10-01",
  "deadline_at": "2026-10-02T04:15:30.000Z",
  "file": {
    "id": "7b8c9d0e-1f2a-4b3c-8d4e-5f6a7b8c9d0e",
    "filename": "truck-1182.mp4",
    "status": "receiving",
    "duration_ms": null,
    "tracks": [
      { "track_id": 1, "type": "video", "codec": "h264", "duration_ms": null },
      { "track_id": 2, "type": "audio", "codec": "aac", "duration_ms": null }
    ]
  },
  "window": { "index": 14, "start_ms": 840000, "end_ms": 900000, "overlap_ms": 0 },
  "media": {
    "expires_at": "2026-10-02T04:15:30.000Z",
    "source_url": "https://editframe.com/api/v1/files/7b8c9d0e-…/tracks/1?media_token=…",
    "index_url": "https://editframe.com/api/v1/files/7b8c9d0e-…/index?media_token=…",
    "audio_url": "https://editframe.com/api/v1/files/7b8c9d0e-…/tracks/2?media_token=…",
    "frames": [
      { "t_ms": 840000, "url": "https://editframe.com/api/v1/files/7b8c9d0e-…/pipeline-frames/…/840000?media_token=…" }
    ]
  },
  "metadata": {
    "transcript": { "metadata": { "word_count": 112 }, "annotations": [] },
    "frames": { "metadata": { "every_ms": 1000, "frame_count": 60 }, "annotations": [] }
  },
  "callback": {
    "url": "https://editframe.com/api/v1/pipeline-callbacks/…",
    "expires_at": "2026-10-02T04:15:30.000Z"
  }
}
  • window is null for a file step. file.status is receiving while a streamed upload is still arriving, and ready after that. Durations are null until the upload is sealed. A file that was uploaded whole also has the probed width and height of video tracks, and sample_rate and channels of audio tracks.
  • deadline_at and callback.expires_at are callback_timeout_ms after the request.
  • metadata holds the outputs of every step this step needs, directly or indirectly, by namespace, with review corrections applied. For a segment step, annotations are limited to the window.
  • media URLs need no API key. They work until media.expires_at, at most four hours after the request. frames lists the frames of every sample_frames step the step needs, at most 1000 per request.
  • idempotency_key is the same for every delivery of one attempt. Use it to deduplicate.

Requests for URL sources

When the run's file was created from a URL, Editframe serves none of its media, and media has your URL instead of Editframe's media URLs. The rest of the request, including file and window, is the same:

json
{
  "media": {
    "url": "https://media.example.com/fleet/truck-1182.mp4",
    "tracks": [
      { "track_id": 1, "type": "video", "label": "camera", "codec": "avc1.640028" },
      { "track_id": 2, "type": "audio", "label": null, "codec": "mp4a.40.2" }
    ],
    "expires_at": "2026-10-02T04:15:30.000Z",
    "frames": [
      { "t_ms": 840000, "url": "https://editframe.com/api/v1/files/7b8c9d0e-…/pipeline-frames/…/840000?media_token=…" }
    ]
  }
}
  • url is the URL that Editframe recorded when it created the file, after redirects. Read it yourself, by byte range for the window of a segment step. Editframe adds no token to it.
  • tracks lists every track with its label and codec, without URLs.
  • source_url, audio_url and index_url are absent. expires_at is present only with frames, whose URLs work until then.

In the SDK, request.media.url is set only for a URL source.

Signatures

Requests are signed with Standard Webhooks headers: webhook-id, webhook-timestamp and webhook-signature (v1, followed by a base64 HMAC-SHA256). Verify them with the endpoint's whsec_ secret and the raw request body:

js
import express from "express";
import { postPipelineCallback, verifyPipelineEndpointRequest } from "@editframe/api/pipelines";

const app = express();

app.post("/editframe/faces", express.raw({ type: "application/json" }), async (req, res) => {
  let request;
  try {
    request = await verifyPipelineEndpointRequest({
      secret: process.env.EDITFRAME_ENDPOINT_SECRET,
      headers: req.headers,
      body: req.body,
    });
  } catch {
    return res.status(401).end();
  }

  // Accept the work now and post the output to the callback URL when it is done.
  res.status(202).end();
  const annotations = await detectFaces(request.media); // your model
  await postPipelineCallback(request.callback.url, {
    status: "succeeded",
    output: { metadata: { face_count: annotations.length }, annotations },
  });
});

For short work, answer with the output instead: res.json({ metadata: { … }, annotations: [ … ] }).

verifyPipelineEndpointRequest accepts a clock difference of 300 seconds by default (toleranceSeconds) and throws PipelineSignatureError when the signature is missing, stale or wrong.

Responses

An output larger than the output limit, or one that is not valid, fails the step.

Callbacks

After a 202, post one of these bodies to callback.url. The URL authenticates the attempt, so send no API key.

json
{ "status": "heartbeat" }
{ "status": "succeeded", "output": { "metadata": {}, "annotations": [] } }
{ "status": "failed", "error": { "message": "Model unavailable", "code": "model_unavailable", "retryable": true } }
  • A heartbeat extends the deadline to callback_timeout_ms from now. Send one before the deadline when work takes longer.
  • error.message has 1–1000 characters. error.code is optional: lowercase letters, digits and underscores, starting with a letter, at most 64 characters. A retryable failure starts another attempt when attempts remain.
  • A callback can arrive before the 202 response. Editframe records it.

Concurrency

Each endpoint step sends at most with.max_concurrency requests at a time, and each endpoint receives at most its own max_concurrency across all steps. An attempt that answered 202 does not hold a slot while it waits for the callback.

Versions

A pipeline has versions numbered from 1. createPipeline creates version 1, and versions cannot be changed. To change a pipeline, create a version:

js
import { createPipelineVersion } from "@editframe/api/pipelines";

const { version, created } = await createPipelineVersion(client, pipeline.id, definition);
// created is false when an existing version has the same digest; that version is returned.
bash
npx editframe pipelines versions create PIPELINE_ID --definition definition.json

A run uses the latest version unless you pass version. Pipeline names are unique among the pipelines of your organization that are not archived. An archived pipeline accepts no new versions or runs (409 pipeline_archived).

Pause a pipeline

Pause a pipeline to stop new work without cancelling runs, for example while your endpoint is down. A pause applies to every version of the pipeline.

js
import { pausePipeline, resumePipeline } from "@editframe/api/pipelines";

const paused = await pausePipeline(client, pipeline.id);
console.log(paused.paused_at); // when the pipeline was paused

await resumePipeline(client, pipeline.id);
bash
npx editframe pipelines pause PIPELINE_ID
npx editframe pipelines resume PIPELINE_ID

While a pipeline is paused:

  • It accepts new versions, runs and groups. New runs stay pending.
  • No pending run starts, and no step, retry attempt or segment window starts.
  • Attempts that started before the pause continue. Editframe records their endpoint callbacks and review decisions, and their runs then wait for the next step.
  • A step that has not started gets no deadline, so the pause cannot make it time out. Attempts that started keep their deadlines, and open reviews keep their expires_at.
  • You can cancel and retry runs. A retried run stays pending until you resume the pipeline.
  • A run that has not started records no usage.

When you resume the pipeline, the waiting runs and steps start immediately, within the limits of their groups and endpoints. Both requests return the pipeline. Pausing a paused pipeline, or resuming a pipeline that is not paused, changes nothing. An archived pipeline cannot be paused (409 pipeline_archived). Pause and resume send no webhooks.

Editframe records each pause and resume, with the API key or user that made it, in the change history of the pipeline in the dashboard. Editframe does not change or delete these records. A request that changes nothing records nothing.

listPipelines takes paused: true to list only paused pipelines (GET /api/v1/pipelines?paused=true). paused=false lists only the pipelines that are not paused.

Runs

Create a run with a pipeline and a video file. The Idempotency-Key header is optional, at most 255 characters, and unique in your organization: a repeated request with the same key and body returns the original run with status 200, and one with a different body returns 409 idempotency_conflict.

bash
curl -X POST https://editframe.com/api/v1/pipeline-runs \
  -H "Authorization: Bearer $EDITFRAME_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: speech-check:7b8c9d0e" \
  -d '{"pipeline_id": "9a8b7c6d-…", "file_id": "7b8c9d0e-…", "external_id": "truck-1182"}'

The response is the run with all of its steps. Steps start once the file's media is available. See Pipeline runs to follow the run.

A run's metadata cannot change after the run is created. It is not the file metadata that steps write. listPipelineRuns takes metadata and returns only the runs that have every entry you give:

js
const { runs } = await listPipelineRuns(client, { metadata: { customer: "acme" } });
// GET /api/v1/pipeline-runs?metadata[customer]=acme
bash
npx editframe pipelines runs create PIPELINE_ID FILE_ID --metadata customer=acme --metadata truck=1182
npx editframe pipelines runs list --metadata customer=acme

Groups

A group holds runs of one pipeline version, for example the uploads of one night. Use a group to follow its runs together, to limit how many of them run at the same time, and to get one notification when they are all finished.

js
import {
  closePipelineGroup,
  createPipelineGroup,
  createPipelineRun,
  getPipelineGroupReport,
} from "@editframe/api/pipelines";

const { group } = await createPipelineGroup(
  client,
  {
    pipeline_id: pipeline.id,
    runs: fileIds.map((file_id) => ({ file_id })),
    max_in_flight: 50,
    metadata: { night: "2026-10-01" },
  },
  { idempotencyKey: "nightly-2026-10-01" },
);

// A run joins a group only when you create it.
await createPipelineRun(client, { group_id: group.id, file_id: lateFileId });

// Close the group after its last run, so it can complete.
await closePipelineGroup(client, group.id);

const report = await getPipelineGroupReport(client, group.id);
bash
npx editframe pipelines groups create PIPELINE_ID --files file-ids.txt --max-in-flight 50 --metadata night=2026-10-01
npx editframe pipelines runs create PIPELINE_ID FILE_ID --group GROUP_ID
npx editframe pipelines groups close GROUP_ID
npx editframe pipelines groups report GROUP_ID
  • runs has 0–1000 entries, each with a file_id or a source_url, and an optional external_id and metadata. The file_id values are distinct. Runs do not get the group's metadata. Unknown files return 404 { "error": "file_not_found", "file_ids": [...] }.
  • version is the pipeline version of every run in the group. Default: the latest version.
  • max_in_flight is 1–100,000, default 100. Other runs of the group stay pending and start oldest first as runs finish.
  • An open group does not complete, also when all of its runs finished. Close it with closePipelineGroup, or create it with close: true (--close) when the request has all of its runs. When you close a group whose runs all finished, it completes immediately. Closing a closed group changes nothing.
  • When you retry a run of a completed group, the group becomes closed again until that run finishes. completion_count is the number of times that the group completed.
  • listPipelineGroups takes status, pipeline_id and metadata filters. listPipelineRuns takes group_id.
  • In a --files file, the CLI reads one file ID per line and ignores text after #.

You can change the max_in_flight of a group in any status. When you raise it, waiting runs start immediately. When you lower it, running runs continue; waiting runs start only when fewer runs than the new limit are running. A run that you retry later uses the limit of its group at that time.

js
import { updatePipelineGroup } from "@editframe/api/pipelines";

await updatePipelineGroup(client, group.id, { max_in_flight: 200 });
bash
npx editframe pipelines groups update GROUP_ID --max-in-flight 200

The body must contain only max_in_flight. A missing value, an unknown field or a value out of range fails with 422 invalid_request. A group that is not in your organization returns 404. A change of max_in_flight sends no webhook.

Editframe records each close and each change of max_in_flight, with the API key or user that made it, in the change history of the group in the dashboard. Editframe does not change or delete these records. Closing a closed group, or a change to the current value, records nothing.

getPipelineGroupReport summarizes the runs of a group:

Creators

Pipelines, versions, endpoints, runs and groups tell who created them:

Both fields are null when Editframe did not record the creator. Runs and groups that an API key created before Editframe recorded creators have that API key in created_by. Other records created before then have null.

The dashboard shows the creator on the page of each record. A record that an Editframe administrator created while viewing your organization shows that an administrator created it.

created_by does not select webhooks. A run sends its webhooks to its API key: the key that created the run through the API, or the key chosen when the run was started in the dashboard.

Streamed uploads

A streamed upload sends a video as verified fragments, so a run can start before the upload finishes. Segment steps process each window as its media arrives. The file has the status receiving until the upload is sealed.

js
import { Client, createPipelineRun } from "@editframe/api/pipelines";
import { uploadFragmented } from "@editframe/api/pipelines/upload";

const result = await uploadFragmented(client, "dashcam.mp4", {
  onCreated: async (file) => {
    await createPipelineRun(
      client,
      { pipeline_id: pipeline.id, file_id: file.id },
      { idempotencyKey: `upload:${file.id}` },
    );
  },
  onProgress: (progress) => console.log(progress.watermarkMs), // verified media time
});
console.log(result.file.status); // "processing" after the seal
bash
npx editframe pipelines upload dashcam.mp4 --run PIPELINE_ID --watch

uploadFragmented accepts a file path, a Blob or File, or a stream. When an upload stops, resume it with fileId (SDK) or --resume FILE_ID (CLI) and the same media. A file has 1–8 tracks and at most one video track. In a browser, direct uploads need CORS on the storage bucket.

URL sources

A run can process a video that you already host at an HTTPS URL, without an upload. Editframe does not copy the media: built-in steps read the byte ranges they need from your URL, and endpoint steps receive your URL. Give source_url in place of file_id:

js
import { createPipelineRun } from "@editframe/api/pipelines";

const { run } = await createPipelineRun(
  client,
  { pipeline_id: pipeline.id, source_url: "https://media.example.com/fleet/truck-1182.mp4" },
  { idempotencyKey: "speech-check:truck-1182" },
);
console.log(run.file_id); // the file Editframe created for the URL
bash
npx editframe pipelines runs create PIPELINE_ID --source-url https://media.example.com/fleet/truck-1182.mp4 --watch

When you create the run, Editframe checks that it can read the URL, reads its tracks and duration, and creates a file of type video for it. The response is the usual run, and its file_id is the new file. The file is ready at once, so every window of a segment step is ready when the run starts. Windows run in parallel up to the step's and endpoint's concurrency limits; nothing waits for media to arrive, as it does with a streamed upload.

Group entries take source_url in place of file_id too:

js
import { createPipelineGroup } from "@editframe/api/pipelines";

const { group } = await createPipelineGroup(client, {
  pipeline_id: pipeline.id,
  runs: [
    { source_url: "https://media.example.com/fleet/truck-1182.mp4", external_id: "truck-1182" },
    { source_url: "https://media.example.com/fleet/truck-1183.mp4", external_id: "truck-1183" },
  ],
  close: true,
});
bash
npx editframe pipelines groups create PIPELINE_ID --source-url URL --source-url URL --close

When a URL cannot be read, Editframe creates nothing: not the run, and for a group request, not the group or any of its runs.

Each source_url in a run or group request creates a new file, and the URLs of one group request must differ. To process one video with several pipelines, create its file once with createFile and give its ID to each run:

js
import { createFile } from "@editframe/api";
import { createPipelineRun } from "@editframe/api/pipelines";

const file = await createFile(client, {
  type: "video",
  source_url: "https://media.example.com/fleet/truck-1182.mp4",
});
console.log(file.status, file.source_url); // "ready", and the URL after redirects

await createPipelineRun(client, { pipeline_id: speechCheck.id, file_id: file.id });
await createPipelineRun(client, { pipeline_id: redaction.id, file_id: file.id });

See Files for the fields of createFile.

Requirements

The URL must meet these requirements when you create the file, and for as long as runs use it, including retries:

  • HTTPS on a public address. Editframe follows up to 3 redirects when it creates the file and records the final URL. Steps read the recorded URL and do not follow redirects.
  • Range requests. A request with a Range header is answered with 206 Partial Content and a Content-Range header that gives the total size.
  • MP4 or MOV. The file is ISO BMFF: progressive, with the moov box anywhere in the file, or fragmented, with a sidx or mfra index.
  • A known duration.
  • Content that does not change. When it creates the file, Editframe records the size and the ETag, or the Last-Modified header when there is no ETag. A step that finds another size, ETag or Last-Modified fails with source_changed.
  • A URL that stays readable. A step that cannot read the URL fails with source_unavailable. A signed URL must not expire while runs use it.

Each track's label is its name in the file: the MP4 udta name, or else the handler name. The with.track option of transcribe, sample_frames and detect_scenes steps selects a track by its label, as for streamed uploads, so give the tracks you select names of lowercase letters, digits and underscores that start with a letter, at most 32 characters. A track whose name does not fit, or that shares its name with another track, has no label. With ffmpeg, set handler names while copying the streams:

bash
ffmpeg -i interview.mp4 -c copy -metadata:s:v:0 handler_name=camera -metadata:s:a:0 handler_name=voice interview-labelled.mp4

URL errors

A URL that cannot be read fails the request with 422 and the code as error. createFile gives the reason as message:

json
{ "error": "source_range_unsupported", "message": "The server answered a Range request with 200 instead of 206." }

createPipelineRun gives the code and the reason as an issue at source_url, and a group request gives an issue for each entry whose URL cannot be read, in entry order, with the first entry's code as error:

json
{
  "error": "source_unreachable",
  "issues": [
    { "path": "runs[3].source_url", "code": "source_unreachable", "message": "…" },
    { "path": "runs[7].source_url", "code": "source_http_error", "message": "…" }
  ]
}

Giving both file_id and source_url fails with 422 invalid_request and the issue not_allowed at source_url; the same URL twice in one group request fails with duplicate.

In the SDK, PipelineApiError has the code in code and the issues in issues, and the error that createFile throws names the code and the reason. Steps of a run whose source later changes or becomes unreadable fail with the step error codes source_changed and source_unavailable.

Outputs

A step output is a JSON object with metadata (an object, nested at most 32 levels) and annotations (an array). Both are optional. An output has at most 1 MiB and 10,000 annotations.

Keyframe boxes use normalized frame coordinates from 0 to 1, with x + w and y + h at most 1. Their t_ms values increase and are between start_ms and end_ms.

Read the outputs of a run, one page per call, with each window's output for segment steps:

js
import { getPipelineRunOutputs, getFilePipelineMetadata } from "@editframe/api/pipelines";

const page = await getPipelineRunOutputs(client, run.id, "detector");
for (const { window, output } of page.outputs) {
  console.log(window?.start_ms, output.annotations.length);
}

// The latest outputs a step wrote to a namespace on a file, from any run.
const latest = await getFilePipelineMetadata(client, fileId, "detector");

A segment output's window has its index, start_ms and end_ms, and the times its latest attempt was planned (created_at), became ready (ready_at), was dispatched (started_at), began on a worker (picked_up_at) and finished (completed_at). A time is null until it happens.

listFilePipelineMetadata(client, fileId) lists the namespaces that steps have written on a file. Use iteratePipelineRunOutputs and iterateFilePipelineMetadata to read every page.

Limits

Errors

Validation errors return 422 with the field paths and issue codes:

json
{
  "error": "invalid_definition",
  "issues": [{ "path": "steps.gate.when.path", "code": "not_upstream" }]
}

The error field is invalid_definition for definitions and invalid_request for other request bodies. Other errors return { "error": code }, for example 409 pipeline_name_taken, 404 pipeline_not_found, 404 pipeline_version_not_found or 404 file_not_found. In the SDK, failed requests throw PipelineApiError with status, code, issues and fileIds.

Routes

All routes are under https://editframe.com and use an API key as a Bearer token, except the callback route. Lists take limit and cursor and return next_cursor, which is null on the last page.

GET /api/v1/pipelines and GET /api/v1/pipeline-endpoints take include_archived=true. GET /api/v1/pipelines also takes paused=true or paused=false. To be notified of run, step, review and group changes, see Webhooks.