AWS Builder Center
Turning a Screenplay Breakdown into an Ontology-Typed Graph with AWS

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)

  1. …
  2. 4
    Turning 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.
  1. The syntactic regime is fixed by the format. A scene opens with an  INT.  or  EXT.  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.
  1. 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.
  1. 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.
  1. A dialogue cue, or character cue, is the character's name in uppercase above their line.
  1. An introductory parenthetical is the parenthesis after a character's first appearance. It gives the age and description.
  1. 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 beats
6 distinct speakers 7 characters, 2 of whom never speak
5 parenthetical ages 0 age properties
LI (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 beats
screenplay 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 ours
nc:sceneTimeOfDay → one of our seven namespaces → counted as ours
ex:Thing → the extractor's built-in vocabulary → counted as foreign
rdfs: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/terms  route.
  • 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  .cql  file 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:
  1. 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.
  1. 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)

  1. …
  2. 4
    Turning a Screenplay Breakdown into an Ontology-Typed Graph with AWS This article
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article