Avails Mapping Strategies for Linear TV
Avails mapping is the engineering problem of turning raw, unstructured inventory exports into a set of validated, monetizable placement windows a scheduler can trust. In a linear broadcast environment an avail is not simply an unprogrammed gap on a transmission grid; it is a constrained asset that must reconcile against contractual obligations, regulatory clearance zones, and frame-accurate playout boundaries. When mapping is done poorly, engines generate phantom inventory, double-book breaks, or misattribute revenue — failures that surface downstream as billing disputes and compliance exposure. This guide sits under Broadcast Traffic Architecture & Taxonomy and details the deterministic resolution and allocation workflow that produces clean avails, aimed at traffic managers who need the operational framing and at Python automation developers who need deployable, production-grade code.
Concept & Data Model
An avail is the join point between three otherwise independent domains: the programming grid (what airs and when), the inventory plan (what is available to sell), and the contract layer (what has been sold and under what constraints). Mapping resolves all three into a single normalized record. The canonical entity hierarchy runs spot → avail → order → campaign; avails mapping operates at the avail level, consuming the field definitions established in Understanding Broadcast Spot Schemas and Metadata and emitting records the scheduler places spots into.
The mapping engine reads a normalized subset of the avail schema. The fields below are the ones resolution actually evaluates; the ingestion boundary guarantees their types before a record reaches the mapper.
| Field | Type | Constraint | Role in mapping |
|---|---|---|---|
avail_id |
str (UUIDv5) |
non-null, namespace-bound, unique | Idempotency key; deduplicates windows across re-exports |
program_id |
str |
FK → program grid | Binds the avail to a program boundary and daypart |
daypart_code |
str (enum) |
controlled vocabulary | Temporal classification for rate-card lookup |
break_position |
int |
≥ 0, ordered within program | Distinguishes A/B/C pods and local windows |
duration_sec |
int |
> 0, ≤ break length | Rejects zero-duration and over-length avails |
clearance_zone |
str (enum) |
national / regional / local | Drives blackout and split-feed logic; never defaults |
inventory_type |
str (enum) |
network / affiliate / local | Determines which contract pool may claim it |
feed_offset_sec |
int |
timezone-resolved | Aligns split-feed markets to a single UTC reference |
rate_card_id |
str |
FK → rate card | Monetization join key |
status |
str (enum) |
forecast → reserved → mapped → aired | The state-machine position of the avail |
Reliable mapping depends on treating these states as a strict progression rather than mutable flags. Without a canonical state model, a re-run of the same export can silently promote a reserved avail back to forecast and reopen inventory that has already been sold. The mapper must therefore behave as a deterministic state machine: it ingests raw grid exports, applies immutable rules, and emits allocation records, and it never mutates the source export in place. Raw avails are parsed into an isolated staging representation where conflict resolution, inventory claiming, and state tagging occur, so a partial write can never corrupt the source of truth. The versioning and rollback discipline that keeps those transitions auditable is covered in Best Practices for Avails Inventory Tracking.
Figure — Avails mapping pipeline: inventory definitions become avail slots, and the mapping engine applies three ordered, deterministic rules — hard constraints, clearance/eligibility, then priority-with-fallback allocation — to emit validated avails the scheduler places spots into.
Implementation Approach
Two design decisions dominate an avails mapper: how clearance is resolved, and how conflicting claims are ordered.
Deterministic rule evaluation over ad-hoc conditionals. Clearance logic (national vs. regional vs. local blackout) and inventory-type eligibility must be expressed as an ordered, independently testable rule set rather than nested if branches scattered through the code path. Each rule takes a normalized avail and returns either a claim, a rejection with a structured reason, or a pass to the next rule. This is the same layered-constraint pattern used by the scheduler in Spot Scheduling Validation & Rule Engines; keeping the two engines aligned means an avail the mapper marks eligible is never rejected for a clearance reason at schedule time.
Priority-with-fallback allocation over first-come claiming. When several contract pools are eligible for the same window — a common case where a national order and an affiliate order both clear — the mapper resolves the claim by contract priority tier, then by rate-card yield, and only then falls back to the next-best window. Fallback prioritizes inventory preservation over aggressive monetization: a contracted network break is never displaced to satisfy a lower-tier local sale. This ordering is deterministic and replayable, which is what lets an audit reproduce a mapping decision byte-for-byte months later.
Lookup service over inline translation for legacy IDs. Hybrid stacks routinely pair a legacy traffic system with a cloud-native ad server, and the same physical break carries two different identifiers. Rather than translating inline, the mapper resolves legacy identifiers through a read-only, cached lookup that maintains referential integrity across the boundary — the same discipline applied when you map legacy spot IDs to modern schemas. Caching resolved mappings keeps latency bounded during high-volume scheduling windows; version-controlling the translation table keeps silent ID drift out of production.
Billing codes act as the primary join key between an avail and its financial obligation, so a deprecated or mismatched code causes the mapper to misroute premium inventory. Normalizing those codes at the ingestion edge — per Standardizing Billing Codes Across Traffic Systems — is a precondition for correct allocation, not a downstream cleanup step.
Production Python Implementation
The reference implementation below is a typed, deployable mapper. Pydantic v2 models enforce the schema contract at the boundary; a small ordered rule set resolves clearance and eligibility; every state transition emits a structured log line in the traffic-ops format timestamp | level | module | message so SOC 2 and FCC audit trails can be reconstructed from logs alone. It is deterministic and idempotent on avail_id.
from __future__ import annotations
import logging
from datetime import datetime, timezone
from enum import Enum
from pydantic import BaseModel, Field, field_validator
# Traffic-ops structured logging: timestamp | level | module | message
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
)
logger = logging.getLogger("avails.mapper")
class ClearanceZone(str, Enum):
NATIONAL = "national"
REGIONAL = "regional"
LOCAL = "local"
class InventoryType(str, Enum):
NETWORK = "network"
AFFILIATE = "affiliate"
LOCAL = "local"
class RawAvail(BaseModel):
"""A normalized avail as it leaves the ingestion boundary. Types are
already coerced; validators here enforce hard, mapping-level constraints."""
avail_id: str = Field(..., min_length=1) # UUIDv5 idempotency key
program_id: str = Field(..., min_length=1)
daypart_code: str = Field(..., min_length=2)
break_position: int = Field(..., ge=0) # 0-indexed pod within program
duration_sec: int = Field(..., gt=0) # zero-duration avails are illegal
break_length_sec: int = Field(..., gt=0)
clearance_zone: ClearanceZone # never defaults; must be explicit
inventory_type: InventoryType
feed_offset_sec: int = Field(default=0) # split-feed offset from UTC reference
rate_card_id: str = Field(..., min_length=1)
billing_code: str = Field(..., min_length=4)
@field_validator("billing_code")
@classmethod
def billing_code_alnum(cls, v: str) -> str:
code = v.strip().upper()
if not code.isalnum():
raise ValueError("billing_code must be alphanumeric for revenue attribution")
return code
class ContractPool(BaseModel):
"""A pool of demand eligible to claim inventory of a given type/zone."""
pool_id: str
inventory_type: InventoryType
allowed_zones: frozenset[ClearanceZone]
priority_tier: int = Field(..., ge=0, le=9) # 0 = highest priority
yield_index: float = Field(..., ge=0.0) # normalized rate-card yield
class MappedAvail(BaseModel):
avail_id: str
claimed_by: str | None = None
accepted: bool
reasons: list[str] = Field(default_factory=list)
mapped_at: str
class AvailsMapper:
"""Resolves normalized avails against contract pools deterministically.
Rules evaluate in a fixed order so the same inputs always produce the same
mapping; the result is keyed on avail_id and safe to replay during audits."""
def __init__(self, pools: list[ContractPool]) -> None:
# Stable, deterministic evaluation order: highest priority first,
# then highest yield. Ties break on pool_id so the sort is total.
self._pools = sorted(
pools, key=lambda p: (p.priority_tier, -p.yield_index, p.pool_id)
)
def _hard_constraints(self, avail: RawAvail) -> list[str]:
violations: list[str] = []
# An avail must physically fit inside its break.
if avail.duration_sec > avail.break_length_sec:
violations.append("duration_exceeds_break")
# A missing/invalid feed offset would misalign split-feed markets.
if avail.feed_offset_sec < -43200 or avail.feed_offset_sec > 50400:
violations.append("feed_offset_out_of_range")
return violations
def _eligible_pool(self, avail: RawAvail) -> ContractPool | None:
# First pool (in priority/yield order) whose type and clearance match.
for pool in self._pools:
if pool.inventory_type != avail.inventory_type:
continue
if avail.clearance_zone not in pool.allowed_zones:
continue
return pool
return None
def map_avail(self, avail: RawAvail) -> MappedAvail:
now = datetime.now(timezone.utc).isoformat()
violations = self._hard_constraints(avail)
if violations:
logger.warning(
"avail_id=%s program_id=%s accepted=False reasons=%s",
avail.avail_id, avail.program_id, ",".join(violations),
)
return MappedAvail(
avail_id=avail.avail_id, accepted=False,
reasons=violations, mapped_at=now,
)
pool = self._eligible_pool(avail)
if pool is None:
# No eligible demand: preserve the break as unsold rather than
# forcing a non-compliant claim.
logger.info(
"avail_id=%s zone=%s type=%s accepted=False reasons=no_eligible_pool",
avail.avail_id, avail.clearance_zone.value, avail.inventory_type.value,
)
return MappedAvail(
avail_id=avail.avail_id, accepted=False,
reasons=["no_eligible_pool"], mapped_at=now,
)
logger.info(
"avail_id=%s claimed_by=%s tier=%d accepted=True",
avail.avail_id, pool.pool_id, pool.priority_tier,
)
return MappedAvail(
avail_id=avail.avail_id, claimed_by=pool.pool_id,
accepted=True, mapped_at=now,
)
A representative accepted-path log line reads 2026-07-03T14:22:07+00:00 | INFO | avails.mapper | avail_id=A-0912 claimed_by=NAT-Q3 tier=0 accepted=True. Because map_avail is pure over its inputs, replaying the same export during an audit produces identical claims — the property FCC public-inspection evidence and SOC 2 collection both depend on.
Validation & Edge Cases
Broadcast operations generate boundary conditions that a naïve mapper mishandles. Each of the following must be an explicit, tested case:
- Timezone and split-feed offsets. East/West split feeds carry the same
program_idbut different real air times. All comparisons resolve to a single UTC reference usingfeed_offset_sec; skipping this collides two markets onto one avail. Daylight-saving transitions are the classic trap — a fixed offset that is correct in January silently misaligns in July. - Sports and live overruns. When live programming overruns, downstream breaks shift and some forecast avails cease to exist. The mapper must treat a program-boundary change as an inventory invalidation, marking affected avails for re-resolution rather than mapping into a window that no longer airs.
- Preemption tiers. A preemption does not simply free a break; it frees it subject to make-good obligations. Preempted network avails route to make-good handling rather than reopening to general local demand, which would violate the displaced advertiser’s SLA.
- Competitive separation at the avail level. Even before spots are placed, some avails are ineligible for a pool because an adjacent break already carries a competing product category. The mapper honors separation as a claim-time constraint so the scheduler is never handed an avail it cannot legally fill.
- Zero-duration and over-length avails. A
duration_secof 0 or a duration exceedingbreak_length_secis a hard rejection, not a warning. Silent acceptance produces a break the automation system cannot splice, which surfaces as a clipped or dropped spot at air. - Missing clearance zone. A missing
clearance_zonemust never default to national. It raises a validation exception at the boundary and routes the record to a dead-letter queue, because a wrong-zone claim can put a nationally-sold spot into a market that legally blacked it out.
Integration Points
Avails mapping is a middle stage: it consumes normalized inventory from ingestion and produces claim-tagged windows for scheduling and billing.
Upstream — ingestion. Raw avails arrive as heterogeneous payloads: flat CSV from legacy traffic systems, EDI feeds from agency portals, or XML/JSON from cloud schedulers. Normalization and strict typing happen before the mapper ever runs, using the validator pattern documented in Schema Validation with Pydantic for Traffic Data and the format handling in the Avion & Avstar ingestion pipelines. The mapper’s RawAvail model is the contract: if a payload cannot populate it, the record is dead-lettered rather than mapped.
Downstream — scheduling and billing. The mapper emits MappedAvail records over a versioned dispatch contract. The message schema below is what the scheduler consumes; note that it carries the claiming pool and a content hash so the scheduler can verify it is acting on the exact mapping the audit ledger recorded.
{
"avail_id": "6ba7b811-9dad-11d1-80b4-00c04fd430c8",
"program_id": "PGM-44120",
"claimed_by": "NAT-Q3",
"clearance_zone": "national",
"inventory_type": "network",
"duration_sec": 30,
"mapped_at": "2026-07-03T14:22:07+00:00",
"content_hash": "sha256:9f2c…"
}
The billing system joins on rate_card_id and billing_code to attribute revenue, which is why normalization of those codes is a hard upstream precondition rather than a downstream reconciliation.
Compliance & Audit Considerations
Avails mapping is where several regulatory and financial controls are first enforced, and getting them wrong is expensive.
FCC clearance and political inventory. An avail flagged for political inventory inherits lowest-unit-charge and public-inspection-file obligations from the moment it is mapped. The mapper refuses to claim a political avail whose billing code cannot be resolved to a candidate, sponsor, and rate, because a missing disclosure is a violation rather than a soft warning. Clearance-zone enforcement is equally regulatory: a national spot mapped into a blacked-out market is a compliance breach, not a scheduling inconvenience.
Immutable audit ledger. Every claim, every rejection, and every re-resolution is appended to a write-once ledger, with each entry content-hashed and chained to its predecessor. Records are never updated in place; a correction is a new entry that references the original. This is what makes a mapping decision defensible under both FCC inspection and financial audit, and it is why the mapper emits a structured log line for every transition.
SOC 2 reproducibility. Because mapping is deterministic and idempotent on avail_id, evidence collection can replay any historical export and obtain byte-identical claims. That reproducibility is the control — it demonstrates that inventory was allocated by rule, not by manual override.
Troubleshooting & Common Errors
When an avails mapping run misbehaves, these are the named patterns operators hit most often, with root cause and remediation.
| Error pattern | Diagnostic indicator | Root cause | Remediation |
|---|---|---|---|
| Phantom inventory | Mapped avails with no corresponding program in the grid | Program-boundary change (overrun/preemption) not propagated before mapping | Invalidate affected program_id avails, re-ingest the current grid, re-run mapping for the daypart |
| Split-feed collision | Two markets claim the same avail_id; overlapping air times |
feed_offset_sec missing or stale across a DST boundary |
Recompute offsets from a timezone-aware reference; re-normalize before mapping |
| Silent zone default | National spot appears in a blacked-out market | clearance_zone defaulted instead of raising |
Enforce non-null zone at the boundary; route missing-zone records to the dead-letter queue |
| Misrouted premium inventory | High-yield avail claimed by a low-tier pool | Deprecated billing_code broke the contract join |
Re-normalize billing codes at ingestion; re-run mapping after the canonical dictionary is patched |
| Non-idempotent re-run | Second run reopens already-reserved inventory | Source export mutated in place; state regressed | Restore staging isolation (read-transform-write); replay from the last ledger checkpoint |
When mapping encounters systemic anomalies — corrupted grid exports, widespread schema drift, or an upstream scheduler failure — a circuit breaker in the automation layer halts downstream writes once an error threshold is breached, and requires explicit operator acknowledgment before resuming. The breaker state machine is the same one detailed in Spot Scheduling Validation & Rule Engines; wiring the mapper into it prevents a bad export from cascading a batch of phantom claims into the scheduler.
Related
- Best Practices for Avails Inventory Tracking — versioned, idempotent state tracking and as-run reconciliation for mapped inventory.
- Understanding Broadcast Spot Schemas and Metadata — the canonical field definitions the
RawAvailmodel normalizes against. - Standardizing Billing Codes Across Traffic Systems — the billing-code normalization that mapping relies on as its contract join key.
- Spot Scheduling Validation & Rule Engines — the downstream engine that places spots into the windows this mapper produces.
- Broadcast Traffic Architecture & Taxonomy — the parent architecture and entity model that governs avail state transitions.