Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ErsatzTV/legacy/llms.txt

Use this file to discover all available pages before exploring further.

Scripted schedules give you full programmatic control over playout construction by calling ErsatzTV’s Scripted Schedule REST API from a Python script. Where YAML schedules express a fixed declarative loop, a scripted schedule can inspect the current playout context, make branching decisions, query external data, and call any API endpoint in any order. This makes scripted schedules ideal for complex or dynamic channel programming — nightly builds that vary by the day of the week, channels that react to new media additions, or deeply customized marathon layouts that require logic beyond what YAML can express. The API is auto-generated from the OpenAPI specification at ErsatzTV/wwwroot/openapi/scripted-schedule.json. A Python client library (etv_client) is generated from this spec and is imported by your scripts.

How Scripted Schedules Work

When ErsatzTV triggers a scripted playout build, it calls entrypoint.py with four arguments: the ErsatzTV host URL, a build ID (UUID), the build mode (reset or continue), and the name of your script module. The entrypoint imports your script, creates an API client, fetches the current PlayoutContext, and then calls three functions from your script in sequence:
  1. define_content — declare all content sources the playout will use.
  2. reset_playout — (called only in reset mode) set the playout’s starting time and state.
  3. build_playout — add content, padding, and control instructions to the playout.

Build Modes

ModeBehavior
resetClears the existing playout and rebuilds it from scratch. Both reset_playout and build_playout are called.
continueExtends an existing playout from its current end time. Only build_playout is called.

The Entrypoint

The entrypoint script (scripts/scripted-schedules/entrypoint.py) is the runtime harness for all scripted schedules. You do not modify this file — it handles argument parsing, module loading, API client setup, and orchestration:
#!/usr/bin/python3

import argparse
import importlib
import sys

from uuid import UUID

import etv_client
from etv_client.api import ScriptedScheduleApi

def main():
    parser = argparse.ArgumentParser(description="Run an ETV scripted schedule")
    parser.add_argument('host', help="The ETV host (e.g., http://localhost:8409)")
    parser.add_argument('build_id', type=UUID, help="The build ID for the playout")
    parser.add_argument('mode', choices=['reset', 'continue'], help="The playout build mode")
    parser.add_argument('script_name', help="The name of the script module to use (e.g., one)")

    known_args, unknown_args = parser.parse_known_args()

    try:
        script_module = importlib.import_module(f"scripts.{known_args.script_name}")
    except ImportError:
        print(f"Error: cannot find a script file named '{known_args.script_name}.py' in the 'scripts' directory.")
        sys.exit(1)

    configuration = etv_client.Configuration(host=known_args.host)

    with etv_client.ApiClient(configuration) as api_client:
        try:
            define_content = getattr(script_module, 'define_content')
            reset_playout = getattr(script_module, 'reset_playout')
            build_playout = getattr(script_module, 'build_playout')

            api_instance = ScriptedScheduleApi(api_client)

            context = api_instance.get_context(known_args.build_id)

            define_content(api_instance, context, known_args.build_id)

            if known_args.mode == "reset":
                new_context = reset_playout(api_instance, context, known_args.build_id)
                context = new_context or api_instance.get_context(known_args.build_id)

            build_playout(api_instance, context, known_args.build_id)
        except etv_client.ApiException as e:
            print(f"Exception when calling scripted schedule api: {e}\n")
        except AttributeError as e:
            print(f"Error: the '{known_args.script_name}' script is missing a required function. {e}")

if __name__ == "__main__":
    main()
Your script file lives at scripts/<script_name>.py relative to the entrypoint directory and must export exactly three functions: define_content, reset_playout, and build_playout.

Required Script Functions

Every scripted schedule module must implement the following three functions with the exact signatures shown:

define_content(api, context, build_id)

Called first on every build. Use this function to register all content sources the playout will reference. Each add_* call below adds a named content source to the build session.

reset_playout(api, context, build_id)

Called only in reset mode. Use this function to position the playout’s start time (via wait_until or wait_until_exact) and perform any one-time initialization. Return a new PlayoutContext if you want build_playout to see the updated time, or return None to let the entrypoint re-fetch it automatically.

build_playout(api, context, build_id)

Called on every build (both reset and continue). This is where you add the actual programming — content items, padding, EPG groups, watermarks, and overlays. Called in a loop by the playout engine until context.is_done is True.

The PlayoutContext

Every API call that adds or modifies playout content returns (or the entrypoint fetches) a PlayoutContext with four fields:
FieldTypeDescription
current_timedatetimeThe current position of the playout build cursor.
start_timedatetimeThe wall-clock time the playout build started from.
finish_timedatetimeThe target end time for this build cycle.
is_doneboolTrue when current_time has reached finish_time.
Use context.is_done in a loop to continue building until the target time is filled.

Content Registration API

Call these methods inside define_content to register named content sources:
MethodBody SchemaDescription
add_collection(build_id, body)ContentCollectionAdd a named collection by its ErsatzTV collection name.
add_multi_collection(build_id, body)ContentMultiCollectionAdd a named multi-collection.
add_smart_collection(build_id, body)ContentSmartCollectionAdd a named smart collection.
add_show(build_id, body)ContentShowAdd a TV show by title or external GUID.
add_search(build_id, body)ContentSearchAdd content matching a search query string.
add_marathon(build_id, body)ContentMarathonAdd a marathon content source with grouping and ordering options.
add_playlist(build_id, body)ContentPlaylistAdd an existing playlist from a playlist group.
create_playlist(build_id, body)ContentCreatePlaylistDynamically create a playlist for this build session.

Playout Building API

Call these methods inside build_playout to add content and control playback:

Add Content

MethodKey FieldsDescription
add_all(build_id, body)contentAdd every item in the referenced content source. Returns an updated PlayoutContext.
add_count(build_id, body)content, countAdd exactly count items. Supports filler_kind, custom_title, disable_watermarks.
add_duration(build_id, body)content, durationAdd content until the specified duration is filled (e.g., "30 minutes"). Supports trim, offline_tail, discard_attempts, stop_before_end, fallback.

Padding

MethodKey FieldsDescription
pad_to_next(build_id, body)content, minutesAdd content until the next multiple of minutes from the current time (e.g., 30 pads to the next half-hour boundary). Supports fallback, trim, discard_attempts, offline_tail.
pad_until(build_id, body)content, whenAdd content until a specific time of day (e.g., "20:00"). Set tomorrow: true to target that time the next day.
pad_until_exact(build_id, body)content, whenAdd content until an exact datetime timestamp.

Timing Control

MethodKey FieldsDescription
wait_until(build_id, body)whenInsert unscheduled (offline) time until a time of day. Set tomorrow: true to target the next day. Set rewind_on_reset: true to allow the cursor to move backward on reset.
wait_until_exact(build_id, body)whenInsert offline time until an exact datetime timestamp.

Skip and Peek

MethodKey FieldsDescription
skip_items(build_id, body)content, countAdvance the content enumerator by count items without adding them to the playout.
skip_to_item(build_id, body)content, season, episodeAdvance a show’s enumerator to a specific season and episode number.
peek_next(build_id, content)content (path param)Returns a PeekItemDuration object with a milliseconds field representing the duration of the next item in the content source, without advancing the enumerator. Useful for look-ahead logic.

EPG Grouping

MethodKey FieldsDescription
start_epg_group(build_id, body)advance, custom_titleOpen a new EPG guide group. Set advance: false to continue the current group. Set custom_title to override all item titles within the group.
stop_epg_group(build_id)Close the current EPG group.

Overlay Controls

MethodKey FieldsDescription
graphics_on(build_id, body)graphics (list of names), variablesTurn on one or more named graphics elements (lower-thirds, bugs). Pass a variables dict for template substitution.
graphics_off(build_id, body)graphics (list of names)Turn off named graphics elements. Pass an empty list to turn off all active elements.
watermark_on(build_id, body)watermark (list of names)Turn on one or more named channel watermarks.
watermark_off(build_id, body)watermark (list of names)Turn off named watermarks. Pass an empty list to turn off all active scripted watermarks.
pre_roll_on(build_id, body)playlistActivate a pre-roll playlist that plays before each subsequent content item.
pre_roll_off(build_id)Deactivate the active pre-roll playlist.

A Complete Minimal Example

"""
Evening Movies channel: plays one movie per night starting at 8 PM,
pads with trailers to the next half-hour boundary, then goes offline
until 8 PM the next day.
"""

import etv_client
from etv_client.models import (
    ContentCollection,
    ContentSearch,
    PlayoutCount,
    PlayoutPadToNext,
    ControlWaitUntil,
)


def define_content(api, context, build_id):
    """Register all content sources this script uses."""
    api.add_collection(
        build_id,
        ContentCollection(key="movies", collection="Feature Films", order="shuffle"),
    )
    api.add_search(
        build_id,
        ContentSearch(key="trailers", query="type:trailer", order="random"),
    )


def reset_playout(api, context, build_id):
    """On reset, anchor the playout start to 8 PM tonight."""
    api.wait_until(
        build_id,
        ControlWaitUntil(when="20:00", tomorrow=False, rewind_on_reset=True),
    )
    # return None so the entrypoint re-fetches the updated context
    return None


def build_playout(api, context, build_id):
    """Build one night of programming: movie → trailer padding → wait until next 8 PM."""
    while not context.is_done:
        # Open an EPG group for the feature film
        api.start_epg_group(build_id)

        # Play one movie
        context = api.add_count(
            build_id,
            PlayoutCount(content="movies", count=1),
        )

        # Close the EPG group
        api.stop_epg_group(build_id)

        # Fill to the next 30-minute boundary with trailers
        context = api.pad_to_next(
            build_id,
            PlayoutPadToNext(
                content="trailers",
                minutes=30,
                trim=True,
                offline_tail=True,
                filler_kind="postroll",
            ),
        )

        # Wait until 8 PM tomorrow before repeating
        api.wait_until(
            build_id,
            ControlWaitUntil(when="20:00", tomorrow=True, rewind_on_reset=False),
        )

        # Refresh context after waiting
        context = api.get_context(build_id)

Script Creation and Execution Flow

1

Install dependencies

Ensure etv_client is installed in the Python environment used by ErsatzTV. The client is generated from ErsatzTV/wwwroot/openapi/scripted-schedule.json using an OpenAPI Python generator.
2

Write your script module

Create a file at scripts/<your_script_name>.py in the scripted-schedules directory. Implement the three required functions: define_content, reset_playout, and build_playout. Each receives (api: ScriptedScheduleApi, context: PlayoutContext, build_id: UUID) as arguments.
3

Configure the playout in ErsatzTV

In the ErsatzTV UI, navigate to your channel’s Playout settings and set the Schedule Kind to Scripted. Point the Script field to your script module name (without the .py extension). Save the playout.
4

Trigger a reset build

Force a full rebuild by triggering a Reset on the playout. ErsatzTV calls entrypoint.py with mode reset, which runs all three of your functions. Inspect the playout timeline in the UI to verify the output looks correct.
5

Monitor and iterate

Check the ErsatzTV logs for any ApiException or AttributeError messages from the entrypoint. Fix script errors, then trigger another reset. Once the script is working, ErsatzTV will call it automatically in continue mode as the playout needs extending.

Tips and Best Practices

Always check is_done

Wrap your build_playout loop body in while not context.is_done: and refresh context from each API response. This prevents the script from overrunning the build window or running infinitely.

Use peek_next for look-ahead

Call peek_next(build_id, content_key) to inspect the duration of the next item before committing to add_count or add_duration. This enables smarter scheduling decisions, such as skipping a film that is too long to finish before a hard time boundary.

Register all content in define_content

All add_collection, add_show, and similar calls must happen in define_content, not in build_playout. This ensures content is available in both reset and continue mode builds without duplication.

Use EPG groups for multi-segment programs

Wrap related content items in start_epg_group / stop_epg_group calls to merge them into a single EPG entry. This is especially useful when a movie is followed by its trailer or a news block is followed by a station ID.
The full Scripted Schedule REST API reference — including all request and response schemas — is available at /api/scripted-schedule.

Build docs developers (and LLMs) love