Skip to content

Episode Data Model

Linear issues:

Source decisions:

  • Data Access Matrix (10/02/26) — defines visibility and editability per field
  • D2D-2 comments: genre storage design (Dan, 14/02/26), genre inheritance rules (Ben, 03/03/26)
  • CDN x TEP weekly catch-up (13/04/26): auto-generated episode IDs
  • Field Reference v5.4: episode and publication field definitions

Working docs (engineering): 01-data-model-redesign.md, 06-genre-handling.md, 09-id-strategy-and-scenarios.md

Dependencies: XSD v1.1 support — project genre must be optional in validation before genre inheritance can work.

Note: This feature includes the full genre inheritance logic (first episode → project, effectiveGenre, projectGenre denormalization, commissioner genre negation) which was separated from XSD v1.1 support because it depends on the new episode document structure.


The current system stores episode and post-transmission data in sub-documents on the screenProject (companyData and sharedData). This conflates storage location with access control and makes per-episode reporting queries inefficient.

This feature restructures where data is stored:

  • Episode-level data (both pre-TX metadata and post-TX publication events) moves to the existing episodes collection
  • Project-level data stays on the screenProject document in formData
  • The companyData and sharedData concepts on screenProject are removed, replaced by formData with field-level metadata for access control (see Feature 4: Field Locking)

Broadcasters provide two types of data: production metadata (pre-TX) and transmission records (post-TX). Both types of episode-level data are now stored against individual episodes, enabling per-episode reporting. For example, a broadcaster can query “show me all episodes broadcast on BBC One in Q3 2026” directly, without needing to go through the project first.

Production companies continue to manage their projects and episodes through the same TEP interface. The underlying storage change is transparent — episodes are already a separate concept in the UI. Contributors remain linked to episodes as before.

Genre data uses a three-field design on episodes to support efficient reporting while maintaining the inheritance rules agreed with CDN:

  • Episode genre — only present when explicitly set (from XML or manual entry)
  • Project genre (denormalized) — always present on episodes, kept in sync with the project
  • Effective genre — always populated (from episode genre if set, else from project genre). This is the field used for all genre-based reporting queries.

Post-transmission data (publication events) is stored as an array on the episode document. Each publication event records when and where an episode was broadcast or made available on-demand. This data is used for reporting filters only — it is not visible in the project management interface. Only broadcasters provide this data; production companies do not edit it.


  • Episodes store pre-TX data: name, number, slotLength, releaseDate, genre, tags
  • Episodes store post-TX data: publications array with publicationDateTime, channelPlatforms, availabilityMode, windowClosureDateTime, isPrimary, isRepeat, publicationId
  • Episodes store external episode IDs: externalEpisodeIds array with funderCompanyId + episodeId
  • Episodes store maker IDs: makerIds array with funderCompanyId + makerId
  • Episodes store genre, projectGenre, and effectiveGenre fields correctly
  • genre field is only set when the episode has its own explicitly-set genre
  • projectGenre field is always present, reflecting the current project genre
  • effectiveGenre field is always populated (from genre if set, else from projectGenre)
  • When project genre changes, projectGenre and effectiveGenre are bulk-updated on all episodes without their own genre
  • Episodes with their own genre are unaffected by project genre changes (their effectiveGenre stays as their own genre)
  • Genre reporting queries use effectiveGenre only (no $or pattern needed)
  • formData captures all project-level attributes — existing fields (projectName, numberOfEpisodes, deliveryDate, seriesIdentifier) continue to work; new fields (medium, broadcasterProjectType, greenlightDate, genre, commissioningDepartment, tags) are added
  • companyData sub-document is removed from screenProject
  • sharedData sub-document is removed from screenProject
  • companyData and sharedData fields are removed from the screenProject schema (no data migration needed — these fields are not in use)
  • Publication events are stored in the episode’s publications array
  • Multiple publication events per episode are supported (broadcast + on-demand, repeats)
  • Publication events include funderCompanyId to identify which broadcaster provided the data
  • publicationId enables corrections via resubmission (matching by publicationId updates the existing event)
  • { projectId: 1 } on episodes — basic project lookup
  • { companyId: 1 } on episodes — scope by production company
  • { "effectiveGenre.ofcom": 1 } on episodes — genre reporting filter
  • { "publications.publicationDateTime": 1 } on episodes — TX date reporting filter
  • { "publications.availabilityMode": 1 } on episodes — broadcast vs on-demand filter
  • { "tags.name": 1, "tags.value": 1 } on episodes — tag reporting filter
  • { releaseDate: 1 } on episodes — release date filter
  • { "makerIds.funderCompanyId": 1, "makerIds.makerId": 1 } on episodes — maker ID reporting scope
  • { "formData.genre.ofcom": 1 } on screenProjects — project genre reporting filter (new field)
  • { "formData.greenlightDate": 1 } on screenProjects — greenlight date filter (new field)
  • { "formData.deliveryDate": 1 } on screenProjects — delivery date filter (existing field, new index)
  • { "formData.medium": 1 } on screenProjects — medium filter (new field)
  • { "formData.broadcasterProjectType": 1 } on screenProjects — broadcaster project type filter (new field)
  • { "formData.tags.name": 1, "formData.tags.value": 1 } on screenProjects — project tag filter (new field)

{
"_id": "ObjectId",
"projectId": "ObjectId",
"companyId": "ObjectId",
"externalEpisodeIds": [
{ "funderCompanyId": "<funderCompanyId>", "episodeId": "episode789" }
],
"makerIds": [
{ "funderCompanyId": "<funderCompanyId>", "makerId": "maker123" }
],
"name": "Episode 1",
"number": "1",
"slotLength": "PT1H",
"releaseDate": "2024-09-15",
"genre": {
"ofcom": "Drama",
"ofcomSuper": "Entertainment",
"commissioner": "Police Drama"
},
"effectiveGenre": {
"ofcom": "Drama",
"ofcomSuper": "Entertainment",
"commissioner": "Police Drama"
},
"projectGenre": {
"ofcom": "Drama",
"ofcomSuper": "Entertainment"
},
"tags": [
{ "name": "series name", "value": "Line of Duty" },
{ "name": "tx season", "value": "Autumn 2024" }
],
"publications": [
{
"publicationId": "pub-001",
"funderCompanyId": "<funderCompanyId>",
"availabilityMode": "broadcast",
"publicationDateTime": "2024-10-05T21:00:00Z",
"windowClosureDateTime": null,
"channelPlatforms": [
{
"label": "BBC One",
"id": "bbc-one",
"subChannels": [
{ "label": "HD", "id": "hd" }
]
}
],
"isPrimary": true,
"isRepeat": false
}
],
"fieldMetadata": {
"name": { "source": "broadcaster", "channel": "xml", "setBy": "<funderCompanyId>", "lockedAt": "2026-..." },
"genre": { "source": "broadcaster", "channel": "xml", "setBy": "<funderCompanyId>", "lockedAt": "2026-..." },
"slotLength": { "source": "broadcaster", "channel": "xml", "setBy": "<funderCompanyId>", "lockedAt": "2026-..." }
}
}

Removed:

  • companyData — per-funder keyed data area
  • sharedData — cross-funder shared data area

Modified:

  • formData — expanded to capture CDN XML project-level attributes. Mapping to existing vs new fields:

    Existing fields (no changes needed):

    • projectName ← XML @name
    • numberOfEpisodes ← XML @numberOfEpisodes
    • deliveryDate ← XML DeliveryDate
    • seriesIdentifier ← XML @number (series number, e.g., “6”)

    New fields to add:

    • medium ← XML @medium (enum: audio | video)
    • broadcasterProjectType ← XML @type (enum: series | special | feature film). Note: stored separately from existing contentType field — whether these should be merged needs Adam’s input.
    • greenlightDate ← XML GreenlightDate (date, inferred as first XML submission date if omitted)
    • genre ← XML Genres (object: { ofcom, ofcomSuper, commissioner }). This is a structured object separate from the existing contentGenre string field (see resolved decision below).
    • commissioningDepartment ← XML <Programmes> block @commissioningDepartment attribute
    • tags ← XML Tags (array of { name, value })

    Open questions for Adam:

    • Should CDN’s @type (series | special | feature film) map to the existing contentType field, or stay as a separate broadcasterProjectType? The existing values may use different terminology.

    Resolved decisions:

    • contentGenre vs formData.genre (decided 2026-05-01): These remain separate concepts. contentGenre is a flat string field used by TEP/Omni clients for their own content classification. formData.genre is a structured { ofcom, ofcomSuper } object sourced from CDN’s XML genre system (Ofcom regulatory genres) and used for episode genre inheritance. The CDN project form shows formData.genre.ofcom and formData.genre.ofcomSuper instead of contentGenre. Commissioner genre is stored separately in privateCompanyData.commissionerGenre (per TEP-452).
    • Ofcom genre validation list: Not yet available. Genre fields accept free-text strings for now. A validated Ofcom genre list (and the Ofcom → OfcomSuper derivation mapping) should be sourced and enforced in a future iteration.
    • Genre field access: formData.genre.ofcom and formData.genre.ofcomSuper are readable by all roles (shared data) but only editable by funders and system admins. Producers (owners) have read-only access. This is enforced via the myProjectAccess query’s access matrix.

Added:

  • fieldMetadata — inline locking metadata (see Feature 4)
  • externalIds — array-based external ID structure (see Feature 3)

The current three data stores need restructuring:

mapping/stores/ProjectDataStore.ts:

  • Update to write project-level fields to formData (expanded scope)

mapping/stores/FunderDataStore.ts:

  • Remove or repurpose — companyData no longer exists
  • Funder-specific metadata (commissioningDepartment, tags) now goes to formData with appropriate fieldMetadata

mapping/stores/SharedDataStore.ts:

  • Remove — sharedData no longer exists

New: mapping/stores/EpisodeDataStore.ts:

  • Writes pre-TX episode data to the episodes collection
  • Handles genre inheritance logic (effectiveGenre population)
  • Handles projectGenre denormalization
  • Writes post-TX publication events to the episode’s publications array (via upsertPublication method)
  • Upsert by publicationId + funderCompanyId for corrections; if no publicationId, append as new publication event

On episode write:

  1. If episode has its own <Genres> in XML → set genre field, set effectiveGenre = genre
  2. If episode has no <Genres> → do NOT set genre field, set effectiveGenre = current project genre
  3. Always set projectGenre = current project genre (denormalized copy)

On project genre change:

db.episodes.updateMany(
{ projectId: projectId, genre: { $exists: false } },
{ $set: {
projectGenre: newProjectGenre,
effectiveGenre: newProjectGenre
}}
)
// Also update projectGenre on episodes WITH their own genre (for reference, though effectiveGenre is unchanged)
db.episodes.updateMany(
{ projectId: projectId, genre: { $exists: true } },
{ $set: { projectGenre: newProjectGenre } }
)

Removals (companyData/sharedData):

ComponentActionLocation
ScreenProject typeRemove sharedData and companyData fieldsschema.graphql
MutationsRemove updateScreenProjectSharedDataschema.graphql
MutationsRemove updateScreenProjectFunderPrivateDataschema.graphql
Input typesRemove UpdateScreenProjectSharedDataInputschema.graphql
Input typesRemove UpdateScreenProjectFunderPrivateDataInputschema.graphql
Payload typesRemove UpdateScreenProjectSharedDataPayloadschema.graphql
Payload typesRemove UpdateScreenProjectFunderPrivateDataPayloadschema.graphql
Mutation resolversDelete updateScreenProjectSharedData.tsresolvers/Mutations/
Mutation resolversDelete updateScreenProjectFunderPrivateData.tsresolvers/Mutations/
Mutations indexRemove imports for both mutationsresolvers/Mutations/index.ts
Field resolversRemove sharedData and companyData resolversresolvers/Queries/fields.ts
Service layerDelete ProjectSharedDataUpdater.tsserviceLogic/
Service layerDelete ProjectFunderPrivateDataUpdater.tsserviceLogic/
FactoryRemove sharedDataUpdater and funderPrivateDataUpdater methodsserviceLogic/ProjectServiceLogicFactory.ts
ValidationRemove UpdateScreenProjectSharedDataSchema and UpdateScreenProjectFunderPrivateDataSchemavalidation/funderDataSchemas.ts

Additions (new formData fields):

The ScreenProjectFormData type, UpdateScreenProjectFormDataInput, and CreateScreenProjectInput need new fields:

# Add to ScreenProjectFormData type
medium: String # "audio" | "video"
broadcasterProjectType: String # "series" | "special" | "feature film"
greenlightDate: Date
genre: GenreData # new structured type
commissioningDepartment: String
tags: [TagData!]
# New types
type GenreData {
ofcom: String
ofcomSuper: String
commissioner: String
}
type TagData {
name: String!
value: String!
}

The corresponding input types, Zod validation schemas, TypeScript interfaces, and MongoDB validation schema all need updating to match.

Episode type additions:

The existing Episode type needs new fields for publication data and genre:

# Add to Episode type
genre: GenreData # episode-specific genre (if set)
effectiveGenre: GenreData # always populated (own genre or inherited)
projectGenre: GenreData # denormalized project genre
slotLength: String # ISO 8601 duration
releaseDate: Date
tags: [TagData!]
publications: [PublicationEvent!] # post-TX data
# New type
type PublicationEvent {
publicationId: String
funderCompanyId: ID!
availabilityMode: String! # "broadcast" | "onDemand"
publicationDateTime: DateTime!
windowClosureDateTime: DateTime
channelPlatforms: [ChannelPlatform!]!
isPrimary: Boolean
isRepeat: Boolean
}
type ChannelPlatform {
label: String!
id: String
subChannels: [SubChannel!]
}
type SubChannel {
label: String!
id: String
}

Note: Publication events are read-only via GraphQL — they are only written by the XML ingestion pipeline or Ben’s upload tool. No create/update/delete mutations needed for publications at this stage.

Episode mutation updates:

The existing episode mutations (createScreenProjectEpisode, updateScreenProjectEpisode, deleteScreenProjectEpisode) need their input types expanded:

# Update CreateScreenProjectEpisodeInput
input CreateScreenProjectEpisodeInput {
id: ID!
number: Int!
title: String # existing
startDate: Date # existing
deliveryDate: Date # existing
slotLength: String # new
releaseDate: Date # new
genre: GenreInput # new
tags: [TagInput!] # new
}

Field resolvers (resolvers/Queries/fields.ts):

  • Remove sharedData resolver
  • Remove companyData resolver
  • Add/update Episode field resolvers for genre, effectiveGenre, projectGenre, publications, tags, slotLength, releaseDate

Episode resolvers:

  • Update episode creation service to handle new fields
  • Update episode update service to handle new fields and trigger genre inheritance logic (if genre changes on project, bulk-update episodes)

Delete:

  • serviceLogic/ProjectSharedDataUpdater.ts
  • serviceLogic/ProjectFunderPrivateDataUpdater.ts

Update:

  • serviceLogic/ProjectFormUpdater.ts — handle new formData fields (medium, broadcasterProjectType, greenlightDate, genre, commissioningDepartment, tags)
  • Episode creation/update services — handle new episode fields, genre inheritance, effectiveGenre population

ScreenProject MongoDB validation schema (validationSchema.ts):

  • Remove companyData and sharedData from schema
  • Add new fields to formData: medium, broadcasterProjectType, greenlightDate, genre (object), commissioningDepartment, tags (array)

Episode MongoDB validation schema:

  • Add fields: genre (object), effectiveGenre (object), projectGenre (object), slotLength, releaseDate, tags (array), publications (array), externalEpisodeIds (array), makerIds (array), fieldMetadata (object)

No data migration is needed — companyData and sharedData are not currently in use. The changes are:

  • Remove companyData and sharedData from the screenProject MongoDB validation schema
  • Add new fields to the screenProject and episode MongoDB validation schemas
  • Create new indexes on the episodes and screenProject collections
  • Remove code references to companyData/sharedData (data stores, resolvers, service logic, validation)

  • Unit tests for genre three-field logic (all scenarios from doc 06)
  • Unit tests for publication upsert by publicationId
  • Integration tests for the full XML → episode storage pipeline
  • Test that effectiveGenre is always correct after various sequences of project/episode genre updates
  • Test that companyData/sharedData removal doesn’t break existing queries (verify no other code references them)
  • Test that new formData fields can be read and written via GraphQL
  • Test that episode genre/tags/publications are correctly exposed via GraphQL
  • Test that publication events are read-only (no mutations available)
  • E2E tests via test harness for pre-TX and post-TX ingestion writing to episodes