# How outline.jinja and transcript.jinja Drive the Podcast Generation Flow in Open Notebook

> Discover how outline.jinja and transcript.jinja templates structure podcast episodes and generate conversational dialogue using a two-stage LLM pipeline in Open Notebook.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: internals
- Published: 2026-06-06

---

**The `outline.jinja` and `transcript.jinja` templates power a two-stage LLM pipeline inside Open Notebook where the outline template structures the episode and the transcript template generates the conversational dialogue for each segment.**

Open Notebook automates multi-speaker podcast creation through a queued command pipeline that turns source content into structured audio. The **podcast generation flow** is orchestrated by the `generate_podcast` command in [`commands/podcast_commands.py`](https://github.com/lfnovo/open-notebook/blob/main/commands/podcast_commands.py), which uses the **`podcast_creator`** library to render Jinja prompts at distinct stages. Understanding how these templates interact reveals how raw content becomes a scripted, voice-ready episode.

## From API Request to Background Job

The flow starts when a client POSTs to `/podcasts/generate`. The `PodcastService.submit_generation_job` method, implemented in [`api/podcast_service.py`](https://github.com/lfnovo/open-notebook/blob/main/api/podcast_service.py), validates the request and enqueues a Surreal-Commands background job. The queued **`generate_podcast_command`** then executes the core generation logic.

The FastAPI router in [`api/routers/podcasts.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/podcasts.py) exposes this endpoint, but the heavy lifting happens asynchronously inside the command layer to avoid blocking the API.

A typical client request looks like this:

```json
POST /api/podcasts/generate
{
  "episode_profile": "my_episode_profile",
  "speaker_profile": "my_speaker_profile",
  "episode_name": "AI in Medicine",
  "notebook_id": "nb123",
  "briefing_suffix": "Focus on ethical concerns."
}

```

## Profile Loading and Briefing Construction

Before any template is rendered, the command resolves configuration profiles and builds a detailed briefing.

```python

# 1️⃣ Load profiles & resolve model configs

episode_profile = await EpisodeProfile.get_by_name(input_data.episode_profile)
speaker_profile = await SpeakerProfile.get_by_name(episode_profile.speaker_config)

# 2️⃣ Build briefing

briefing = episode_profile.default_briefing
if input_data.briefing_suffix:
    briefing += f"\n\nAdditional instructions: {input_data.briefing_suffix}"

# 3️⃣ Call podcast-creator (internally uses the Jinja templates)

result = await create_podcast(
    content=input_data.content,
    briefing=briefing,
    episode_name=episode_dir_name,
    output_dir=str(output_dir),
    speaker_config=speaker_profile.name,
    episode_profile=episode_profile.name,
)

```

The **`briefing`** string combines the `episode_profile.default_briefing` with an optional **`briefing_suffix`** supplied by the user. This briefing, along with the raw source `content`, is later passed into both Jinja templates to maintain consistency across the episode.

## How outline.jinja Structures the Episode

The first template-driven phase generates the episode skeleton. Inside `podcast_creator`, the outline phase renders `prompts/podcast/outline.jinja` with three variables:

- `briefing` – the episode-level brief.
- `context` – the source material (string or list of strings).
- `speakers` – a list of speaker profiles containing name, backstory, and personality.

The template produces a JSON object with a **`segments`** array. Each segment entry contains a `name`, `description`, and `size` hint (`short`, `medium`, or `long`). The `podcast_creator` library parses this rendered JSON and stores it as `episode.outline` (see lines 56–62 of [`commands/podcast_commands.py`](https://github.com/lfnovo/open-notebook/blob/main/commands/podcast_commands.py)).

A typical outline stored in the database looks like this:

```json
{
  "segments": [
    {"name":"Intro", "description":"Introduce the topic", "size":"short"},
    {"name":"Ethics", "description":"Discuss ethical implications", "size":"medium"},
    {"name":"Future", "description":"Look ahead", "size":"long"}
  ]
}

```

This JSON acts as the creative brief for the transcript generation stage that follows.

## How transcript.jinja Generates Dialogue

For every segment defined in the outline, `podcast_creator` renders `prompts/podcast/transcript.jinja`. This second template receives:

- The same `briefing` and `context`.
- `speakers` (or a single speaker for solo podcasts).
- The **`outline`** generated in the previous step.
- The current `segment` definition.

The template returns a JSON object with a **`transcript`** list, where each element maps a `speaker` name to its `dialogue`. The command layer stores this result in `episode.transcript` (see lines 56–62 of [`commands/podcast_commands.py`](https://github.com/lfnovo/open-notebook/blob/main/commands/podcast_commands.py)).

The transcript structure saved to the episode record follows this format:

```json
{
  "transcript": [
    {"speaker":"Dr. Ada", "dialogue":"Welcome..."},
    {"speaker":"Prof. Turing", "dialogue":"From an ethical standpoint..."}
  ]
}

```

Because the template is invoked once per segment, the final assembled transcript concatenates multiple segment outputs into a continuous conversational script ready for text-to-speech rendering.

## Persisting the Final Episode

After `create_podcast` finishes processing all segments, the `generate_podcast` command persists a **`PodcastEpisode`** record. The database object attaches:

- `outline` – the raw JSON produced by `outline.jinja`.
- `transcript` – the raw JSON produced by `transcript.jinja`.
- `audio_file_path` – the final audio file generated by the TTS step.

The simplified database view looks like this:

```json
{
  "id": "episode-abc123",
  "name": "AI in Medicine",
  "briefing": "...",
  "outline": {
    "segments": [
      {"name":"Intro", "description":"Introduce the topic", "size":"short"},
      {"name":"Ethics", "description":"Discuss ethical implications", "size":"medium"},
      {"name":"Future", "description":"Look ahead", "size":"long"}
    ]
  },
  "transcript": {
    "transcript": [
      {"speaker":"Dr. Ada", "dialogue":"Welcome..."},
      {"speaker":"Prof. Turing", "dialogue":"From an ethical standpoint..."}
    ]
  },
  "audio_file": "/data/podcasts/episodes/xyz/audio.mp3"
}

```

## Summary

- The **podcast generation flow** in Open Notebook uses a queued command architecture to transform source material into spoken audio.
- The **`generate_podcast_command`** in [`commands/podcast_commands.py`](https://github.com/lfnovo/open-notebook/blob/main/commands/podcast_commands.py) loads profiles, builds a briefing, and delegates template rendering to the `podcast_creator` library.
- **`outline.jinja`** receives `briefing`, `context`, and `speakers`, and emits a JSON episode structure with a `segments` array.
- **`transcript.jinja`** receives the outline, current segment, and speaker data, and emits a JSON dialogue script for that segment.
- The command stores both JSON artifacts plus the final `audio_file_path` in the `PodcastEpisode` record.

## Frequently Asked Questions

### What triggers the podcast generation flow in Open Notebook?

A client POST request to `/podcasts/generate` triggers `PodcastService.submit_generation_job`, which enqueues a Surreal-Commands background job. The queued `generate_podcast_command` then executes the outline and transcript template pipeline asynchronously.

### What variables does outline.jinja receive during rendering?

The `outline.jinja` template receives `briefing` (the episode brief), `context` (source material), and `speakers` (a list of profile objects with name, backstory, and personality). It must render a JSON object containing a `segments` array with `name`, `description`, and `size` fields.

### How does transcript.jinja know what to write for each segment?

The `transcript.jinja` template is rendered once per segment. It receives the full `outline`, the current `segment` definition, the original `briefing` and `context`, and the `speakers` list. This ensures each segment's dialogue stays aligned with the overall episode structure and source material.

### Where are the generated outline and transcript stored?

The `generate_podcast` command saves the rendered JSON from both templates directly into the `PodcastEpisode` database record as `outline` and `transcript`. It also attaches the `audio_file_path` produced by the downstream TTS engine.