
Turning a Screenplay Breakdown into an Ontology-Typed Graph with AWS
A screenplay-to-graph pipeline on AWS: a published ontology constrains the LLM, so extraction returns typed data instead of a lossy table.
Series: Agentic Graph for Media Industry (4 articles)
- …
- 4Turning a Screenplay Breakdown into an Ontology-Typed Graph with AWS This article
Context
A feature-length screenplay arrives in a production department as a hundred-page PDF. Before a
shooting schedule, a budget, or a location scouting brief can exist, someone reads those hundred pages by hand. They copy the structured data into a multi-tab spreadsheet: the scenes, the sets, the
characters, the vehicles, the stunts, and the effects. This work is called the breakdown , the parsing of a screenplay into production elements. It is the first link in a chain that everything else depends on.
This problem recurs across production groups. It grows worse as soon as a company tracks several films in parallel. Such a portfolio of active projects is called a slate. At that scale, the breakdown
stops being a task and becomes a bottleneck. Four roles depend on it, and they do not want the same thing.
- The production coordinator does the breakdown by hand. They work fast when nothing else lands the same week. They do not want speed from automation. They want an output that matches the exact taxonomy of their own templates. They also want markers that point to where to look, instead of forcing a full re-read.
- The head of physical production covers the whole slate and does not read screenplays. Their job is to know whether three films can enter preparation at once without colliding over cast, crew, and budget. They cannot answer that question when the third breakdown is still two weeks away. They also need one taxonomy across territories, so they can compare two co-productions directly.
- The first assistant director consumes the breakdown rather than producing it. They turn it into the shooting schedule, and they do so quickly when the breakdown is clean. The breakdown is never clean on day one. A scene marked interior instead of exterior, or day instead of night, becomes a scheduling error. That error is paid in shooting hours once the crew discovers it on set. This role is the one to study most closely when building the automation, because it does not drive the tool yet bears the cost of its mistakes.
- The line producer budgets from the shooting schedule. They sit furthest downstream, so they absorb every upstream delay.
None of these roles asks for a language model that reads screenplays. They ask for reliable structured data. The architecture that follows answers the point where their needs meet.
The problem
Two workflows start from the same material and never talk to each other
The first workflow is sequential. The breakdown comes first, then the shooting schedule built from it, then the budget built from that schedule. A feature's breakdown takes one to two days, and the budget takes several weeks. No step can start before the previous one is frozen. A delay at the top therefore moves down to every step below it. Shooting dates make this worse, because they are pinned to financing windows that do not move.
The second workflow is parallel and disconnected. A storyboard artist re-reads the screenplay. A location manager re-reads it for scouting. A producer draws a pitch deck from it. Each person produces an artifact linked to none of the others. Rename one character, and three people must update three documents by hand.

Figure 1. The two workflows, and why the second one shapes the architecture. At the top, the
sequential chain: a delay on the breakdown reaches all the way to the budget. At the bottom, four
people re-read the same screenplay independently and produce artifacts that nothing ties together.
Speeding up one step of the top chain gives a bounded gain. Giving the bottom group a common data
substrate removes the redundant re-readings and the divergences between artifacts.
This second workflow shapes the architecture. Speeding up a breakdown from two days to fifteen minutes is a real gain, but it touches only one step. Giving the screenplay's five consumers one shared data substrate is a gain of a different kind.
The problem with simply handing a screenplay to a language model
A screenplay is a document with two regimes.
- The syntactic regime is fixed by the format. A scene opens with an
INT.orEXT.line that carries the location and the time of day. A name in uppercase above a line marks who is speaking. An age sits in parentheses at a character's first appearance.
- The semantic regime needs reading. It covers knowing that a line of action describes a stunt, that an object is a vehicle rather than a prop, or that a scene reverses the balance of power between two characters.
A single model call treats both regimes the same way. It interprets the part that the format had already guaranteed. It does not count the headings; it decides which ones deserve to become a scene. The result shows no visible error. It is plausible and well-formed, yet it has dropped facts that were written in the file.
A spreadsheet cannot tell you what it missed
The first version of the product is obvious. A model receives the screenplay and returns a table to
open in a spreadsheet.
Four craft terms help in reading what follows.
- A scene heading, or slug line , is the line that opens a scene. It carries three fixed-position fields: the interior or exterior token, the location, and the time of day.
- A dialogue cue, or character cue, is the character's name in uppercase above their line.
- An introductory parenthetical is the parenthesis after a character's first appearance. It gives the age and description.
- A beat is a unit of dramatic structure, a moment of the story. It has no typographic boundary in the screenplay.
Fountain syntax defines the first three terms. It is the text format that screenplays arrive in. It does not define the fourth. The test used a synthetic screenplay. It carried seventeen scene headings, seventy-one dialogue cues for six distinct speakers, and five parenthetical ages. The resulting spreadsheet, the first surface the product exposed, contained only seven scenes.
text
in the submitted file in the extracted graph--------------------- ----------------------17 scene headings 7 scenes, named after the beats6 distinct speakers 7 characters, 2 of whom never speak5 parenthetical ages 0 age propertiesLI (age 10), 1 line absent from the graph
No part of this run looked like a failure. No call errored, the exit code was zero, and the graph was
well-formed, with seven scenes correctly typed and plausibly labeled. Yet the scenes carried beat
names from the statement of intent, such as "Scene 4 - Catalyst", instead of the real headings. Ten
headings produced no scene at all. This is silent under-extraction, and it is a costly failure mode
because nothing signals it.
The lesson is not that the model read the screenplay wrong. The real problem is harder. In a spreadsheet, an empty cell has two meanings that look identical. It can mean no vehicle appears in the scene, or it can mean the extraction did not report a vehicle. The first statement describes the
screenplay. The second describes the extraction. Only the second was true, and the spreadsheet hid the difference.
The solution
The target is a graph rather than a spreadsheet. A graph is a set of nodes, such as scenes, characters, locations, and objects. Typed edges connect those nodes, with labels such as "appears
in" or "takes place at". A spreadsheet places rows side by side with no link between them. A graph
instead connects the entities, and every node and edge carries a type from a published, versioned
vocabulary. The spreadsheet then becomes one projection of that graph, alongside the shooting schedule and the location scouting brief.
The pipeline is split by determinism rather than by service. Whatever can be computed without a
model is computed offline and for free. The model receives only what is left.

Figure 2. The architecture in three bands. Layer 1 is extraction. The screenplay is linted, then split into two documents. The
slug-line pass computes offline what is deterministic, and Perseus receives the documents together with the ontology. The interlink order is not cosmetic: the slug-line graph goes first because it settles property conflicts. Layer 2 is loading, in five stages, OMC foundation first, then a check that every requested edge exists. Layer 3 is review. The read service assembles its responses from two sources, the graph database for topology and the run's on-disk artifacts for the RDF types, across eleven routes.The vocabulary is the contract
The vocabulary of the result has to be chosen before any extraction begins. This project does not
invent a new production vocabulary. The foundation layer is the Ontology for Media Creation from MovieLabs, an RDF specification published by a consortium of studios. RDF is a standard for writing such a graph in a shareable form. Every type and every link is named by a web address called a URI. Two tools that use the same URI therefore refer to the same thing. The OMC foundation supplies
omc:NarrativeScene , omc:NarrativeLocation , omc:Character , and omc:NarrativeObject .Four analysis layers sit on top, each in its own namespace, because OMC describes production rather than narrative. They cover dramatic structure, narrative functions, character archetypes, and an interpretation vocabulary.

Figure 3. The vocabulary submitted for extraction. The OMC foundation carries the production entities. The four analysis layers anchor onto it through their own object properties, and that anchoring makes a reading of the narrative queryable from a scene. The green border marks the only two classes that a deterministic pass creates, with no model call. One label detail says something about the extraction. OMC declares very specific sub-properties, such as
omcT:aNarrativeSceneFeatures.Character , while the extractor emits their parent properties
omcT:features and omcT:featuresIn . The labels therefore carry what is actually produced, not what
is only declarable.What Lettria and Perseus bring
Extraction runs on Perseus , the text-to-graph service from Lettria . Lettria is an AWS Partner, and Perseus is available on AWS Marketplace . A short introductory video presents the service. This choice shapes the rest of the project, and it is more than a simple API call.
The input to Perseus is the project's ontology, not a prompt. Perseus accepts an OWL document in
Turtle. OWL is the dialect of RDF that declares a vocabulary, meaning its classes and properties.
Turtle is the text format that holds it. Perseus stores the ontology, then accepts a job bound to it. The returned graph is typed in the project's own vocabulary, with its classes and object properties. This removes any intermediate mapping, so nothing extra needs maintaining when the vocabulary changes. The sequence takes six calls:
Python
client = perseus_client.PerseusClient()ontology = client.ontology.upload_ontology("ontology/narrative-ontology.ttl")document = client.file.upload_file(run_dir / "scenario-screenplay.md")job = client.job.submit_job(file_id=document.id, ontology_id=ontology.id)job = client.job.run_job(job.id)client.job.download_job_output(job.id, base)graph = client.ttl.parse_ttl_to_knowledge_graph(ttl_path.read_text(encoding="utf-8"))
submit_job is the only point where the ontology and the document meet, and it makes the returned graph conform to the contract. Three further properties of the service each removed a comparable amount of code.The graph model and its serializers. An extraction returns a single object that serializes to disk
in three forms: Turtle, Cypher, and JSON. Cypher is the language of graph databases, the equivalent of SQL for a graph. A serialization in Cypher is therefore a file of write commands. The
three forms describe the same graph. This lets the pipeline validate the semantic side in RDF and load the property-graph side in Cypher from one extraction. It writes no third serializer, and it avoids any risk that separate serializers diverge.
Idempotence by content fingerprint. Perseus registers each ontology and document by hash. Resubmitting identical bytes returns the existing registration instead of creating a second one, so it
consumes no quota.
Graph merging.
interlink combines several graphs into one. Its resolution rule is what matters.
When two graphs assert different things about the same entity, the graph seen first wins.Python
merged = client.interlink([deterministic_graph, model_graph])
This rule makes the split in the next section possible. The pipeline parses the scene headings itself
and passes that graph first. A heading read by a parser then wins over the same heading interpreted by a model. Without a documented, stable priority order, the conflicts would have to be settled by hand afterward.
Why not the AWS GraphRAG Toolkit
The AWS GraphRAG Toolkit powers another demonstration: a coaching agent that answers by citing a method book. A two-part post, The coaching agent that can't fake a citation, built on AWS , describes that pipeline. The two approaches are opposite, and the comparison clarifies this choice.
The toolkit builds a lexical graph with six fixed labels: source, chunk, topic, statement, fact, and entity. It stores them in Amazon Neptune Analytics , with the vectors in Amazon S3 Vectors . It industrializes the whole text-to-graph process. It also builds a provenance chain,
fact → statement → topic → chunk , that makes every assertion traceable to a line of the book. For verifiable citation, it is the right tool.The toolkit does not constrain the semantics, by design. Its node model is closed, so it accepts neither OWL nor Turtle. Its only typing lever is a list of preferred entity classifications, which the
extractor can set aside when a passage does not fit. This is a preference rather than a constraint. The toolkit also names relation predicates freely, without a controlled vocabulary, so two edges with the same meaning can carry different names. It does not expose entity resolution either. In the book example, the word "desire" appears as five separate nodes, because an entity's identity combines its value and its classification. The part-1 post states the trade-off directly: this graph retrieves well, and it does not reason precisely.
The two tools therefore fit different questions. For the question, where does this sentence come from, rigor can sit downstream. The toolkit retrieves the passage, and an in-house schema with a
word-for-word check validates it afterward. For the question, which exterior night scenes involve a
minor,
omc:NarrativeScene , nc:sceneTimeOfDay , and nc:characterAge must mean the same thing on
every run. That rigor has to sit upstream, in the ontology submitted to the extractor. A lexical graph
does not answer the second question, and a graph typed by a published ontology does.Splitting the pipeline by determinism
The source is not a PDF. It is Fountain , a text screenplay format whose syntax guarantees certain things. A scene heading starts with
INT. or EXT. and carries its three fixed-position fields. A dialogue cue is a line in uppercase. An age sits in the introductory parenthetical.One detail of format has a consequence. Perseus accepts
.txt and .md , but not .fountain . The
pipeline therefore converts the Fountain file to Markdown, and it submits that derived Markdown for extraction. It records the Markdown fingerprint in the provenance. The deterministic pass instead reads the original Fountain, because the Fountain syntax carries the guarantees. There are two formats and two readers, with one traced conversion between them.What the syntax guarantees should not be asked of a model. A parser reads it without error and
without varying between runs. The deterministic pass therefore runs before any network call and builds its own graph. It creates one scene per heading and one location per distinct set, with the ordinal, the setting, the time of day, and the source line bounds.
Extraction handles only what truly requires reading: the stunts, the effects, the props, the relationships between characters, and the intentions.
Don't mix the statement of intent with the screenplay
One result of this pipeline is surprising, and it fits in a single line. Submitting the screenplay and
its statement of intent in one document halves the extraction of the screenplay.
text
full document (screenplay + statement of intent) 7 scenes, named after the beatsscreenplay only 14 scenes, named after the headings
Analytical prose holds more interpretable statements than the screenplay, so it draws the extractor's attention. The conversion now produces two documents, submitted as two jobs and then merged. The deterministic pass goes first in the merge, and the
interlink rule makes that order reliable.The ontology-application verdict is measured, not assumed
Submitting an ontology does not prove that it was applied. A graph typed in a foreign vocabulary can still look perfectly healthy.
This became an architectural property rather than a one-time fix. On every run, the pipeline checks
that the returned graph really uses the project's vocabulary, and it reports the result.
The check is mechanical. Every entity type and every predicate in the graph carries a URI, and each
URI belongs to a namespace. A URI that starts with one of the project's seven published namespaces is one of its terms. Any other URI is a foreign term. The technical namespaces that all RDF carries, namely
rdf: , rdfs: , owl: , xsd: , and two others, count on neither side, because they appear whatever vocabulary is applied.text
omc:NarrativeScene → one of our seven namespaces → counted as oursnc:sceneTimeOfDay → one of our seven namespaces → counted as oursex:Thing → the extractor's built-in vocabulary → counted as foreignrdfs:label → technical namespace → ignored
Two numbers come out of this count. The pipeline writes them into the provenance and prints them to standard output. One of the four possible verdicts is
ontology-job-ignored . It means the ontology
was submitted, the job succeeded, and not one of the project's terms came out. That is the case no team wants to discover three weeks later. The same judge counts the relations that carry one of the
project's properties, so the two figures cannot contradict each other.Loading, in stages
The loader writes the graph into a graph database such as Amazon Neptune . Such a database stores nodes and edges directly, where a conventional database would arrange tables. The rest of this post calls it the store. Loading runs in five stages: preflight; a statement plan, meaning the Cypher write
commands, with validation; writing the nodes; writing the relations; and an identity check that re-reads the artifacts and confirms that every requested edge exists.
One source of vocabulary, three consumers
The vocabulary has one author and three consumers.
- Perseus receives it with the screenplay and constrains the extraction with it.
- The read service, a NestJS BFF (Backend For Frontend), serves a copy of it on the
GET /ontology/termsroute.
- The web application, a Vite and React SPA, queries that route instead of reading a file.
The last two consumers do not read the Turtle. They read a projection, which is the same vocabulary re-serialized as JSON. The projection is a file-to-file transformation that runs offline and for free.
Each projected term carries its URI, its human label, its definition, its layer, and its Cypher label. The next block shows the term
omc:NarrativeScene as the generator emits it, and the full projection holds 108 such terms:JSON
{"generatedFrom": "apps/mcp-scenario/ontology/narrative-ontology.ttl","sourceSha256": "4657f1c479d5b5f384cf67798933a61e730505904a15de723702c6d7a0e3bbb8","terms": [{"uri": "https://movielabs.com/omc/rdf/schema/v2.8#NarrativeScene","curie": "omc:NarrativeScene","cypherLabel": "omc_NarrativeScene","label": "narrative scene","comment": "A self-contained unit of action and dialogue within the narrative of a Creative Work, identified by a scene number and slug line. Each scene is the primary anchor for narrative-craft constructs (value shifts, revelation points, etc.) that are scoped via an attached NarrativeCraftContext. Source: omc.ttl:1336.","kind": "class","layer": "omc","domain": [],"range": []}]}
The next block shows the first scene of the screenplay, as the extraction serializes it for the store:
the node, then its properties. It is written in Cypher, the equivalent of SQL for a graph. A file of
such write commands uses the
.cql extension, and the loader replays that file:cypher
MERGE (n:`omc_NarrativeScene` {uri: 'https://w3id.org/ai-creative-assistant/ns/slugline-pass#scene-ea8d143ac0b4388e'})SET n += {`rdfs_label`: 'EXT. MERIDIAN DEEP-SEA STATION — EXTERIOR — DAY (FLASHBACK)',`omc_hasSlugline`: 'EXT. MERIDIAN DEEP-SEA STATION — EXTERIOR — DAY (FLASHBACK)',`nc_sceneOrdinal`: 1, `nc_sceneSetting`: 'EXT', `nc_sceneTimeOfDay`: 'DAY (FLASHBACK)',`nc_sourceLineStart`: 90, `nc_sourceLineEnd`: 96};
The two blocks together show the problem clearly. The node carries no URI. It carries
omc_NarrativeScene , and its properties carry omc_hasSlugline and nc_sceneOrdinal . Those strings
are all an application receives from the store. cypherLabel is therefore the field that does the work. One naming rule turns the URI omc:NarrativeScene into the string omc_NarrativeScene . Two separate programs must apply that rule the same way, or the interface and the store stop describing the same thing. The two programs are easy to confuse, so it helps to separate them.- The loader takes the
.cqlfile that Perseus produces and runs its commands to fill the store. It never recomputes a label. It replays exactly what the Perseus Cypher serializer wrote. Reimplementing the naming rule here would risk drifting from it.
- The generator does not touch the store. It reads the Turtle, meaning the vocabulary and its 108 terms, and re-applies the same rule to each term. It produces the JSON projection served to the interface.
The two programs work on different material. The loader fills the graph with the instances of one
screenplay, the scenes of this film. The generator publishes the dictionary of types, the vocabulary that is the same for every screenplay. They meet on a single string,
cypherLabel .This shared string gives the projection its role as a reverse-lookup table, an index read backwards. The store returns only raw labels, so an application that receives
omc_NarrativeScene can display only that string. The projection makes the return trip, from the label to its meaning. It maps omc_NarrativeScene to its URI, its CURIE omc:NarrativeScene (the short form of the URI), its label
"narrative scene", its definition, and its layer. Without the projection, the interface could show only
technical identifiers.The user experience
A breakdown is not an extraction. It is an extraction that has been reviewed. This half of the system
is a web application and the service that feeds it. Two decisions do most of the work here. The read
path cannot write to the graph, and the interface states what it does not know.

Figure 4. The produced graph, as the reviewer sees it. The reference run holds 165 entities and 327 relations, the same figures as the manifest. Matching those figures is the simplest check that the API
reconstructs what the extraction wrote. The legend is the ontology layer, so the five layers of Figure
3 appear here as colors, including
propp: for narrative functions. The right-hand panel shows the selected character with its URI, its age in nc:characterAge , its relations named by their property, and the twenty scenes where it is declared. Each node is sized by the number of relations that touch it, which makes the load-bearing entities stand out without inventing a score.Opening an entity shows the other half: what the graph asserts about it, and how to contest that claim.

Figure 5. The detail of an entity. The left-hand list is grouped by ontology layer, such as "OMC foundation, 84 entities". Each entry shows its type as a CURIE, for example
omc:Character or omc:NarrativeLocation . This is the vocabulary projection at work. The store returned only the label
omc_Character , and the screen shows the human label, the CURIE, and the layer because the projection can invert them. On the right, each property shows its restored RDF type, for example nc:characterAge 38 xsd:integer . The relations are grouped by the property that names them, nc:hasArchetype and then nc:hasPsychProfile with its five Big Five scores. Every relation, not only every entity, offers its two named outcomes and a button that shows the matching passage of the screenplay.Two registers, and the screen says which one it speaks
Two audiences read this application, and they do not share a vocabulary. A head of production wants to know what the screenplay is. A reviewer wants to know whether the extraction got it right. The navigation separates the two views. No ontology term, URI, or fingerprint appears on the production side, and a test enforces this by checking the rendered text against every published prefix.

Figure 6. The Production view, the "what the screenplay is" side. It shows thirty-two scenes,
seventeen named locations, six characters, and twenty-two objects. The casting load ranks characters by number of scenes, and "Li" appears with three scenes. Li is the character that the first version had
missed. Two sentences on this screen carry the discipline of the project. The first: "every figure
counts something the extraction reported, nothing is estimated, and nothing is converted into days."
The second, under the casting load: "a scene is not a shooting day, and this analysis does not
schedule, so no day count can be read from these figures." The screen refuses to produce the number its reader would want most. Where two location entries might name the same set, it shows them paired and states that the decision is a human judgment, recorded in the review.
Human judgment lives outside the graph's lifecycle
Human judgment has to live somewhere, and it does not live in the graph. It lives in Amazon
DynamoDB , in a single table, with one partition per run.
text
pk = RUN#<runId>sk = ENTITY#<uri> correction on an entity| RELATION#<source>|<predicate>|<target> correction on a relation| CONTEXT declared language and territory
The pipeline applies corrections on top of the graph at read time, and never writes them into it. Three consequences follow. The extracted graph stays exactly as extracted. A database reset cannot take the human work with it. The reviewer always sees the machine extraction with a visible human layer on top, rather than a merged artifact whose origin has been erased.
Each correction carries the digest of the extraction it was made against. Staleness then becomes
computable rather than guessed. A correction whose digest no longer matches is not merged. It surfaces separately, with both digests shown.
Making absence visible
This is the one decision the project would keep above all others, and the review queue shows it best.

Figure 7. The review queue on the reference run. Three things stand out. The categories at zero are
not hidden; each carries its reason, such as "this analysis does not read layout, so a misplaced title
cannot surface here." The three outcomes of the decision card are named by their effect: keep, reject,
and correct the name. Each one shows what it will write into the human layer, rather than a generic
accept or reject. The most honest line on the screen concerns provenance. The extraction recorded no
screenplay line for this character, so the interface searches the document for the string, finds two occurrences, and warns that this result is your reading of the document, not a claim by the extraction. Line 22 there shows, in passing, "her younger brother LI (10)", the character that the
first version of the system had lost.
The breakdown grid applies the same principle, column by column.

Figure 8. The breakdown grid. The deterministic pass fills the columns for scene number, interior or
exterior, location, and time, across all thirty-two scenes. Vehicles and props are filled when the extraction classified the object, such as "Submersible", "Emergency bulkhead lever", and "CASS drone".
Four columns hold no published term: extras, stunts, visual effects, and notes. Each shows a dash that
carries its reason. The reason is the cell's accessible name, read by a screen reader and shown on
hover. A bare dash would say "this scene has no stunt", which is a claim about the screenplay. The
reason instead says "nothing in the vocabulary can report a stunt", which is a claim about the
extraction. Only the second is true.
Conclusion
You now know how a screenplay extraction pipeline can return a typed graph rather than a spreadsheet. You have seen where to place the boundary between what is parsed and what is extracted. You have also seen how to keep human judgment outside the graph's lifecycle. If you build something similar, follow four steps. Publish the vocabulary before you write the extractor. Submit it to a service that can conform to it. Measure on every run that the service applied it. Finally, make your interface state what it does not know.
Two takeaways:
- Do not ask a model for what the format already guarantees. The ten lost scenes were present in the submitted file. Splitting the pipeline by determinism returned more value than any prompt tuning, and the result no longer varies between runs.
- An empty cell is a claim, so choose which claim it makes. "No vehicle in this scene" and "the extraction does not report a vehicle" are different statements. The second one revealed everything this post describes.
Export to the industry's scheduling tools does not exist yet, and that feature will decide whether the first assistant director adopts the result.
Series: Agentic Graph for Media Industry (4 articles)
- …
- 4Turning a Screenplay Breakdown into an Ontology-Typed Graph with AWS This article
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article