CaptionPass
ProductConvertLearnPricingDocsRoadmap
Sign inDashboardConvert & fix free
CaptionPass

Professional caption delivery QA for editors and producers.

Product

  • Free caption converter
  • Fix captions
  • Pricing
  • What is CaptionPass?
  • API
  • Sign in
  • Dashboard
  • Roadmap

Learn

  • SRT vs VTT
  • YouTube caption upload issues
  • Why your captions are not showing
  • Fix overlapping subtitles
  • Educators
  • All guides →

Legal

  • info@captionpass.com
  • Privacy
  • Terms

© 2026 CaptionPass. Caption reliability layer for delivery.

Docs · Custom exports (Account)

Custom export scripts (CPES)

Built-in presets (YouTube SRT, WebVTT, LMS TTML, and others) cover most freelance deliveries. Pro adds CaptionPass Export Scripts — small JavaScript functions you save on your account and reuse on every project when a client, network, or MAM needs something presets do not ship out of the box.

Why use a custom script?

  • Partner specs — required filename, header block, or line-length rules that differ from our YouTube/TikTok presets.
  • Sidecar files — a JSON manifest, CSV review sheet, or plain-text log alongside SRT/VTT in a handoff zip.
  • Transform before emit — uppercase for broadcast, merge lines, strip markers, or split cues using the same mezzanine IR you already edited in the workbench.
  • Repeatable handoffs — bundle built-in presets plus your scripts in a named custom handoff profile so one click reproduces the full client pack.

CPES is not a second caption editor: you still fix timing and text in the project workbench. The script only runs at export time on the saved timeline.

Input → output (what actually flows)

Input is your project's mezzanine timeline — an array of cues with millisecond timings and line text (the same structure as CaptionPass JSON IR). You do not upload a file to the script; the workspace passes the current cues after your edits.

Example input (two cues, abbreviated):

[
  { "index": 1, "startMs": 1200, "endMs": 4200, "lines": ["Welcome back to the channel."] },
  { "index": 2, "startMs": 4200, "endMs": 8100, "lines": ["Today we fix", "overlapping subs."] }
]

Output is whatever you return: one file per script run — SRT, VTT, JSON, CSV, or plain text — with a filename you choose. Helpers like ctx.emit.srt(cues) run the same delivery rules engine as built-in presets, optionally with overrides (e.g. shorter max line length).

Example A — partner-branded SRT

Prepends a client header, then emits SRT with a tighter line length than the default YouTube preset.

function exportTimeline(cues, ctx) {
  var body = ctx.emit.srt(cues, { maxLineLength: 38 });
  return {
    filename: "partner-delivery.srt",
    content: "PARTNER-ID: EXAMPLE\n" + body,
    contentType: "text/plain; charset=utf-8"
  };
}

Example output:

PARTNER-ID: ACME-2026-Q2
PROJECT: Client Reel v3

1
00:00:01,200 --> 00:00:04,200
Welcome back to the channel.

2
00:00:04,200 --> 00:00:08,100
Today we fix
overlapping subs.

Example B — broadcast all-caps

Transforms cue text, then emits SRT. Useful when delivery notes say "uppercase on all lines" but you still want readable mixed case while editing.

function exportTimeline(cues, ctx) {
  var upper = cues.map(function (c) {
    return {
      index: c.index,
      startMs: c.startMs,
      endMs: c.endMs,
      lines: c.lines.map(function (l) { return l.toUpperCase(); })
    };
  });
  return {
    filename: "broadcast-upper.srt",
    content: ctx.emit.srt(upper, { maxLineLength: 32 }),
    contentType: "text/plain; charset=utf-8"
  };
}

Example C — JSON manifest for MAM / producers

No subtitle text — a machine-readable sidecar with project title, IR revision, cue count, and span duration. Add this template to a handoff profile next to SRT and VTT files.

function exportTimeline(cues, ctx) {
  var durationMs = cues.length
    ? cues[cues.length - 1].endMs - cues[0].startMs
    : 0;
  var manifest = {
    project: ctx.project.title,
    irRevision: ctx.project.irRevision,
    cueCount: cues.length,
    durationMs: durationMs,
    exportedAt: new Date().toISOString()
  };
  return {
    filename: "delivery-manifest.json",
    content: JSON.stringify(manifest, null, 2),
    contentType: "application/json"
  };
}

Example output:

{
  "project": "Client Reel v3",
  "irRevision": 4,
  "cueCount": 2,
  "durationMs": 6900,
  "exportedAt": "2026-05-22T18:00:00.000Z"
}

Example D — CSV review sheet for QC

Spreadsheets for client QC or translation vendors: one row per cue with times in milliseconds. You can open this in Excel without parsing SRT.

function exportTimeline(cues, ctx) {
  var rows = ["cue_index,start_ms,end_ms,text"];
  for (var i = 0; i < cues.length; i++) {
    var c = cues[i];
    var text = c.lines.join(" / ").replace(/"/g, '""');
    rows.push(
      c.index + "," + c.startMs + "," + c.endMs + ',"' + text + '"'
    );
  }
  return {
    filename: "review-sheet.csv",
    content: rows.join("\n"),
    contentType: "text/plain; charset=utf-8"
  };
}

Validate script vs Test on project

In Account → Custom exports, two buttons exercise your script differently. Do not use Validate when you need to see real project output.

ControlWhat it does
Validate scriptSyntax check plus a QuickJS dry-run on two sample cues. Fast; does not read your project timeline.
Test on projectFull run on the selected project IR — shows output filename and preview. Requires a ready project with cues loaded.

Deeper click-path notes: Dashboard Help → Custom exports.

Script contract

Define exportTimeline(cues, ctx):

  • cues — read-only array from your mezzanine timeline
  • ctx.emit.srt / vtt / ttml / json — run delivery rules and emitters
  • ctx.time.formatSrt / formatVtt — format timestamps if you build text manually
  • ctx.project — title, irRevision

Return:

{ filename: "partner.srt", content: "...", contentType: "text/plain; charset=utf-8" }

Save templates under Dashboard → Account → Custom exports. Use Single export → Custom on a project, or add templates to a custom handoff profile for zip downloads.

Custom handoff profiles

A profile is an ordered list: any built-in preset, any saved CPES template, or both. Example pack for a corporate client: YouTube SRT + WebVTT + delivery-manifest.json (script) in one zip — same workflow as the built-in Freelance handoff profile, but with your filenames and sidecars.

End-to-end workflow

  1. Upload and fix cues in a Pro project workbench (mezzanine IR is the source of truth).
  2. Author exportTimeline in Library → Custom exports → Validate, then Test on project.
  3. Attach the template to a custom handoff profile (Account → Custom exports) or Quick export on Deliver.
  4. Download handoff zip — receipt JSON is inside the archive; optional export webhook fires on success.

API parity: POST /api/v1/export-templates/validate and POST …/{id}/test — see v1 API.

Common CPES errors

SymptomFix
exportTimeline is not definedDeclare global function name exactly exportTimeline.
Empty zip entryReturn non-empty content and a valid filename.
Test mode needs projectPick a ready project UUID from Library test dropdown or URL path.

Limits (fair use)

Script size32 KiB max per template
Output size2 MiB max per file
Timeline sizeSame cue limits as standard exports
ScopePro workspace only; scripts cannot call external services or load modules. Dashboard export today — not the public HTTP API.

Integrators using Bearer keys: see HTTP API (v1) for preset-based processing. CPES is for signed-in Pro editors in the project workspace.