Skip to content
screenjson

For developers & platform teams

ScreenJSON for Developers

The developer's view of ScreenJSON. Schema shape, UUID discipline, language maps, where to hook in, and how the four tools compose into a pipeline.

Last updated January 2026

What this is, from a developer’s perspective

ScreenJSON is a JSON Schema for screenplays, a CLI that reads and writes it, an embeddable viewer that renders it, and a batch microservice that orchestrates it. If you’ve ever had to integrate a screenplay into a web app, a content pipeline, a storage system, or an AI workflow, it’s the component that was previously missing.

Concretely:

  • The schema is published at https://screenjson.com/schema.json and conforms to JSON Schema. Your existing validators work out of the box.
  • The CLI is a single Go binary (Docker image, too), reads and writes FDX, Fountain, FadeIn, PDF, JSON, and YAML, and exposes the engine as a REST API in server mode.
  • The viewer is a framework-agnostic JavaScript library with a Svelte-native component, available via NPM or CDN.
  • Greenlight is the batch tier: Go backend, Redis queue, Svelte admin UI, pluggable blob storage.

You can adopt any subset of these. They compose, they don’t depend on each other at runtime, and every interface between them is declared.

Read the schema first

Everything else makes more sense once the schema is in your head. The shortest summary:

  • One document, one top-level object.
  • Required at the root: id, version, title, lang, charset, dir, authors, document.
  • document contains a cover, a scenes array, optional layout, and optional bookmarks.
  • Every scene has an id, a heading (structured slugline), a body (typed elements), and taxonomy arrays (cast, props, sfx, locations, and so on).
  • Every scene-body element extends a common base that carries id, scene back-reference, authors, notes, revisions, and meta.
  • Every textual field is a language map keyed by BCP 47 ("en", "fr-CA", "ja").
  • Extension data lives in meta, nowhere else.

Read the full walkthrough on the specification page. The remainder of this guide assumes you have.

UUID discipline

Every node that can be referenced across a document carries an RFC 4122 UUID. This is the single most important design decision in the schema, and the one that, if you don’t internalise it, will cause you problems later.

  • A character appears in the characters index once, with a UUID.
  • Every dialogue and character (cue) element references that UUID — not the string “MARA”.
  • Notes, bookmarks, and element revisions all reference UUIDs, too.

Implication: when your code renames a character, you change one string in the index. Everything that points at the UUID keeps working. When your code diffs two drafts, you compare by UUID, and “MARA has three more lines in draft 4” becomes a trivial query.

Implication for generators: if you’re producing ScreenJSON from somewhere other than the CLI, use a proper UUID generator, and keep UUIDs stable across regenerations unless the node has genuinely changed identity.

The language-map invariant

Every user-facing textual field is a map, not a string:

{ "title": { "en": "The Heist" } }
{ "text":  { "en": "We have ninety seconds.", "fr": "Nous avons quatre-vingt-dix secondes." } }

Two consequences:

  1. You cannot render text without picking a language. The CLI, viewer, and exporters all accept a --lang flag and fall back predictably.
  2. You cannot assume a key exists. A document that declares "lang": "en" may still carry optional "fr" translations; a document that declares "lang": "fr" may not have "en". Handle absence gracefully.

Three helpers most integrations end up writing:

function pickText(
  map: Record<string, string>,
  preferred: string,
  fallback = 'en',
): string {
  return map[preferred] ?? map[fallback] ?? Object.values(map)[0] ?? '';
}

Hook points

You don’t integrate with “ScreenJSON” — you integrate with one of the concrete tools. Here’s where each one fits.

The REST API (from screenjson-cli)

Run the CLI in server mode and every conversion, export, and validation is an HTTP call:

MethodPathBody
POST/convertmultipart/form-data with a file, or application/octet-stream with X-Input-Format
POST/exportScreenJSON JSON body + X-Output-Format header
POST/validateScreenJSON JSON body → { valid, errors[] }
GET/formatssupported formats and capabilities
GET/healthliveness

Drop the container behind an API gateway, add your own auth, expose it.

The viewer (screenjson-ui)

Embed as a script tag (zero build) or import as an ESM module. The library exposes a constructor, a Svelte component, and TypeScript types.

import ScreenJSONUI, { type ScreenJSONDocument } from '@screenjson/ui';

const viewer = new ScreenJSONUI({
  element: '#viewer',
  src: '/screenplay.json',
  theme: 'dark',
});

Greenlight

Greenlight’s API accepts job submissions and streams progress over WebSocket. It expects content on an S3-compatible store (S3, MinIO, Azure Blob), and emits results back to the same. Pipelines are declarative task lists — you can define your own in config/tasks.yml.

Direct schema validation

If you don’t want to pull in any ScreenJSON tool at all, just validate against the public schema with whatever JSON Schema validator you already use — Ajv (JS), jsonschema (Python), gojsonschema (Go), JSV (Rust).

import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import schema from 'https://screenjson.com/schema.json' assert { type: 'json' };

const ajv = addFormats(new Ajv({ allErrors: true }));
const validate = ajv.compile(schema);
const ok = validate(document);

Storage & serialisation

ScreenJSON documents are “just” JSON, so any storage that accepts JSON works. But the schema is designed to play particularly well with:

  • MongoDB / document stores — the top-level object maps straight to a document; UUIDs make excellent secondary keys; analysis is trivial to shard off to a sibling collection.
  • DynamoDB — partition on id, sort on version (or revision.label), keep analysis in a dedicated table.
  • Elasticsearch — map title, logline, heading.setting, per-language dialogue text; index by character id.
  • Object storage — one file per document, with content-addressable keys derived from the document UUID.

The CLI has first-party drivers for Elasticsearch, MongoDB, Cassandra, DynamoDB, Redis, S3, Azure Blob, and MinIO — useful when you want the converter to write directly without an intermediate filesystem step.

On backwards compatibility

The schema is semver. Minor-version bumps add fields. Major-version bumps can remove them. Your validator tells you which version a document was authored against; your pipeline should refuse documents whose major version it doesn’t know.

Every object has meta, a string-to-string map, for data the schema doesn’t know about. If your tool wants to round-trip its own metadata, put it in meta. Anywhere else is non-conformant.

Operational notes

  • Encryption. AES-256-CTR over text runs, SHA-256 key derivation. Encrypted documents still expose structure, UUIDs, and metadata to indexers. Keys are app-level concerns; the CLI reads from SCREENJSON_ENCRYPT_KEY.
  • PDF import. Requires Poppler’s pdftohtml. The Docker image bundles it.
  • PDF export. Optional Gotenberg integration for HTML→PDF rendering. Without it, the CLI emits a best-effort plain PDF.
  • Workers. Server mode runs a worker pool sized to CPU by default; override with --workers.

Next