Version: 1.0 App: Scriptty — an offline screenwriting application for Malayalam and English writers.
This document defines the JSON structure of .screenplay files. Any tool or LLM that produces a valid JSON file conforming to this spec can generate screenplays openable in Scriptty.
A .screenplay file is a UTF-8 encoded JSON object with five top-level keys:
{
"content": { },
"meta": { },
"settings": { },
"story": { },
"scene_cards": [ ]
}All five keys should be present. Missing keys will be filled with defaults, but for maximum compatibility, include all of them.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
title |
string | yes | "" |
Screenplay title. Appears on the title page and section headers in PDF exports. |
author |
string | yes | "" |
Writer name(s). For multiple writers, use " & " for writing teams (e.g. "Joel & Ethan Coen") or " and " for sequential writers (e.g. "Scott Frank and Jon Cohen"). |
director |
string | no | "" |
Director name. If same as author (case-insensitive), the app renders "Written and Directed by". |
contact |
string | yes | "" |
Contact info (email, phone, agent). May contain newlines (\n). |
draft_number |
integer | yes | 1 |
Draft revision number, starting at 1. |
draft_date |
string | yes | "" |
Human-readable date, e.g. "2026-03-14" or "March 14, 2026". |
created_at |
string | yes | "" |
ISO 8601 timestamp of document creation, e.g. "2026-01-15T09:30:00Z". |
updated_at |
string | yes | "" |
ISO 8601 timestamp of most recent save. |
extra |
object | no | {} |
Non-standard title-page metadata preserved for Fountain round-trip — string-to-string map. Holds Fountain title-page keys that don't map to a first-class meta field (e.g. Source, Copyright, custom keys). Original key spelling preserved. Omitted from the JSON when empty so legacy files stay byte-clean on resave. |
Example:
"meta": {
"title": "Monsoon Wedding",
"author": "Sabrina Dhawan",
"director": "Mira Nair",
"contact": "sabrina@example.com",
"draft_number": 3,
"draft_date": "2026-03-14",
"created_at": "2025-11-01T10:00:00Z",
"updated_at": "2026-03-14T18:45:00Z"
}| Field | Type | Required | Default | Allowed Values |
|---|---|---|---|---|
font |
string | yes | "manjari" |
"noto-sans-malayalam", "manjari" |
default_language |
string | yes | "malayalam" |
"malayalam", "english" |
input_scheme |
string | yes | "mozhi" |
"mozhi", "inscript1", "inscript2" |
These control editor display and input behavior. For LLM-generated files, use the defaults or set default_language to match the screenplay's primary language.
Example:
"settings": {
"font": "noto-sans-malayalam",
"default_language": "malayalam",
"input_scheme": "mozhi"
}Pre-writing material stored alongside the screenplay. All fields are plain text (no markup).
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
idea |
string | yes | "" |
Logline or core premise. Typically 1–3 sentences. |
synopsis |
string | yes | "" |
Full story arc in prose. Typically 300–800 words. |
treatment |
string | yes | "" |
Scene-by-scene narrative breakdown. Can be thousands of words. |
narrative |
string | no | "" |
Long-form prose version of the story. Free-form, no length limit. |
These fields are optional in practice — empty strings are valid. They can be included in PDF exports.
Example:
"story": {
"idea": "A retired detective in Kochi receives a letter from a murder victim — posted the day before the killing.",
"synopsis": "Inspector Menon, forced into early retirement after a corruption scandal...",
"treatment": "",
"narrative": ""
}An array of scene card objects. Each card stores user-written notes for a specific scene. If no cards have been created, use an empty array [].
| Field | Type | Description |
|---|---|---|
scene_index |
integer | Zero-based index matching the scene's position in the screenplay. The first scene heading is index 0, the second is index 1, etc. |
description |
string | What happens in the scene (2–4 lines typical). Fountain synopses (= ...) imported into Scriptty land here — and they're emitted back as = lines on Fountain export, so this field is semi-public. |
shoot_notes |
string | Production notes: equipment, VFX, stunts, location details. Also the storage site for Fountain inline notes ([[ ... ]]) and section headers attached during import — see the section-marker convention below. |
Scene metadata like location, time of day, characters, and page count are computed automatically from the screenplay content — they are not stored in scene cards.
When a Fountain file is imported, section headers (# Act One, ## Sequence A, …) attach to the next scene's shoot_notes with an internal marker so the round-trip back to Fountain can re-emit them at the right depth:
[[#section depth=1]] Act One
Multiple lines (sections + free-form notes + inline [[ ... ]] notes) are joined with \n in the same field. The Fountain exporter recognises the marker prefix and emits a # Act One heading before the scene; lines without the marker emit as inline [[ ... ]] notes. Don't author the marker syntax by hand — let the importer manage it.
Example:
"scene_cards": [
{
"scene_index": 0,
"description": "Establishing shot. Menon alone in his apartment, reading the letter.",
"shoot_notes": "Practicals only — desk lamp, TV glow. Handheld camera."
},
{
"scene_index": 1,
"description": "Flashback to the police station, six months earlier.",
"shoot_notes": "Period dressing for the station. 30 extras needed."
}
]This is the screenplay itself, stored as a ProseMirror document tree in JSON format.
{
"type": "doc",
"content": [
// Array of screenplay element nodes
]
}The content array must contain one or more block-level element nodes. An empty screenplay has a single empty scene heading:
{
"type": "doc",
"content": [
{ "type": "scene_heading" }
]
}Every screenplay element is a JSON object with this structure:
{
"type": "<element_type>",
"content": [
{ "type": "text", "text": "The actual text content" }
]
}type— one of the six element types listed below.content— an array of text nodes. For an empty element, omit thecontentkey or use an empty array[].
Text nodes are always { "type": "text", "text": "..." }. There are no marks (bold, italic, underline) — all text is plain. Multiple text nodes in a single element are concatenated.
The slug line that opens each scene. Conventionally written in UPPERCASE.
Format: INT./EXT. LOCATION - TIME OF DAY
Common prefixes: INT., EXT., INT./EXT., I/E.
Common time suffixes: DAY, NIGHT, DAWN, DUSK, CONTINUOUS, LATER, MOMENTS LATER
{ "type": "scene_heading", "content": [{ "type": "text", "text": "INT. COFFEE SHOP - DAY" }] }
{ "type": "scene_heading", "content": [{ "type": "text", "text": "EXT. BEACH - NIGHT" }] }The app auto-uppercases text typed into scene headings. When generating files, always write scene headings in UPPERCASE.
Scene description, stage direction, or narrative prose. Written in normal sentence case.
{ "type": "action", "content": [{ "type": "text", "text": "Ramesh walks through the crowded market, scanning faces." }] }For multiple paragraphs of action, use separate action nodes — one per paragraph.
The name of the character who is about to speak. Always in UPPERCASE. May include extensions in parentheses.
Common extensions: (V.O.) (voice over), (O.S.) (off screen), (O.C.) (off camera), (CONT'D) (continued).
{ "type": "character", "content": [{ "type": "text", "text": "RAMESH" }] }
{ "type": "character", "content": [{ "type": "text", "text": "MEERA (V.O.)" }] }The app auto-uppercases text typed into character elements. When generating files, always write character names in UPPERCASE.
The spoken words of the character named in the preceding character element. Written in normal sentence case.
{ "type": "dialogue", "content": [{ "type": "text", "text": "I've been waiting for you." }] }Dialogue can be in any language, including Malayalam Unicode text:
{ "type": "dialogue", "content": [{ "type": "text", "text": "ഞാൻ നിന്നെ കാത്തിരിക്കുകയായിരുന്നു." }] }Mixed-language dialogue is fully supported:
{ "type": "dialogue", "content": [{ "type": "text", "text": "അവൻ flat ലേക്ക് പോയി." }] }Brief acting direction for the character, placed between the character name and dialogue (or between dialogue lines). Written in lowercase.
Important: Do NOT include parentheses in the text content. The app adds them automatically for display and export. Store only the inner text.
{ "type": "parenthetical", "content": [{ "type": "text", "text": "whispering" }] }
{ "type": "parenthetical", "content": [{ "type": "text", "text": "to Meera" }] }
{ "type": "parenthetical", "content": [{ "type": "text", "text": "beat" }] }Editing instructions, typically right-aligned. Written in UPPERCASE, usually ending with TO:.
{ "type": "transition", "content": [{ "type": "text", "text": "CUT TO:" }] }
{ "type": "transition", "content": [{ "type": "text", "text": "FADE OUT." }] }
{ "type": "transition", "content": [{ "type": "text", "text": "SMASH CUT TO:" }] }Use transitions sparingly — modern screenwriting style often omits them.
Screenplay elements follow conventional ordering patterns. While the format does not enforce strict sequencing, well-formed screenplays follow these rules:
- A screenplay typically opens with a
scene_heading(or optionally atransitionlike"FADE IN:"). - A
scene_headingis followed by one or moreactionelements. - A dialogue block is a sequence:
character→ optionalparenthetical→dialogue. Multiple rounds ofparentheticalanddialoguecan follow a singlecharacter. - After dialogue, the screenplay returns to
actionor starts a new dialogue block with anothercharacter. - A
transitionis typically followed by ascene_heading.
Valid dialogue block patterns:
character → dialogue
character → parenthetical → dialogue
character → dialogue → parenthetical → dialogue
character → parenthetical → dialogue → parenthetical → dialogue
Invalid patterns to avoid:
dialoguewithout a precedingcharacter— orphaned dialogueparentheticalwithout a nearbycharacter— orphaned direction- Two consecutive
characterelements — missing dialogue scene_headingimmediately followed bycharacter— usually needs anactionbeat first (though not strictly forbidden)
A short bilingual screenplay demonstrating all element types:
{
"content": {
"type": "doc",
"content": [
{
"type": "scene_heading",
"content": [{ "type": "text", "text": "INT. RAMESH'S APARTMENT - NIGHT" }]
},
{
"type": "action",
"content": [{ "type": "text", "text": "A small, dimly lit apartment. Books and newspapers are stacked on every surface. RAMESH (50s), unshaven, sits at a desk reading a handwritten letter. His hands tremble." }]
},
{
"type": "character",
"content": [{ "type": "text", "text": "RAMESH" }]
},
{
"type": "parenthetical",
"content": [{ "type": "text", "text": "reading aloud" }]
},
{
"type": "dialogue",
"content": [{ "type": "text", "text": "\"നിങ്ങൾ ഇത് വായിക്കുമ്പോൾ ഞാൻ ജീവിച്ചിരിക്കില്ല...\"" }]
},
{
"type": "action",
"content": [{ "type": "text", "text": "He sets the letter down. Stares at the postmark date." }]
},
{
"type": "character",
"content": [{ "type": "text", "text": "RAMESH" }]
},
{
"type": "parenthetical",
"content": [{ "type": "text", "text": "to himself" }]
},
{
"type": "dialogue",
"content": [{ "type": "text", "text": "ഇത് ഇന്നലെ post ചെയ്തതാണ്." }]
},
{
"type": "action",
"content": [{ "type": "text", "text": "His phone RINGS. He picks it up." }]
},
{
"type": "character",
"content": [{ "type": "text", "text": "MEERA (V.O.)" }]
},
{
"type": "dialogue",
"content": [{ "type": "text", "text": "Ramesh, have you seen the news?" }]
},
{
"type": "character",
"content": [{ "type": "text", "text": "RAMESH" }]
},
{
"type": "dialogue",
"content": [{ "type": "text", "text": "What news?" }]
},
{
"type": "character",
"content": [{ "type": "text", "text": "MEERA (V.O.)" }]
},
{
"type": "parenthetical",
"content": [{ "type": "text", "text": "beat" }]
},
{
"type": "dialogue",
"content": [{ "type": "text", "text": "Suresh is dead." }]
},
{
"type": "action",
"content": [{ "type": "text", "text": "Ramesh looks at the letter. Then at the phone. His expression hardens." }]
},
{
"type": "transition",
"content": [{ "type": "text", "text": "SMASH CUT TO:" }]
},
{
"type": "scene_heading",
"content": [{ "type": "text", "text": "EXT. POLICE STATION - DAY" }]
},
{
"type": "action",
"content": [{ "type": "text", "text": "Ramesh walks up the steps of the station he was forced to leave six months ago. He pauses at the entrance, then pushes through the doors." }]
}
]
},
"meta": {
"title": "The Last Letter",
"author": "Arun Kumar",
"director": "Arun Kumar",
"contact": "arun@example.com\n+91 98765 43210",
"draft_number": 1,
"draft_date": "2026-03-14",
"created_at": "2026-03-14T10:00:00Z",
"updated_at": "2026-03-14T10:00:00Z"
},
"settings": {
"font": "noto-sans-malayalam",
"default_language": "malayalam",
"input_scheme": "mozhi"
},
"story": {
"idea": "A retired detective receives a letter from a murder victim — posted the day before the killing.",
"synopsis": "",
"treatment": "",
"narrative": ""
},
"scene_cards": []
}- Character names must be UPPERCASE in
characterelements. - Scene headings must be UPPERCASE and follow the
INT./EXT. LOCATION - TIMEformat. - Parentheticals must NOT include parentheses — store
"whispering", not"(whispering)". The app wraps them automatically. - Each text block is a separate node. Don't put multiple paragraphs in one
actionnode — use oneactionnode per paragraph. - Malayalam text is standard Unicode. Mixed-language text within a single element is normal and expected.
- No formatting marks. There is no bold, italic, or underline — the schema has no marks. Emphasis is conveyed through CAPS, word choice, and parentheticals.
- The
contentarray in a text-bearing element should contain{ "type": "text", "text": "..." }objects. For empty elements, omitcontentor use[]. - The file extension must be
.screenplay. - Encoding must be UTF-8.
- Transitions are optional and used sparingly in modern screenwriting.
CUT TO:between every scene is considered outdated.