# How the Open Notebook REST API Handles Podcast Episode Retry on Failure

> Learn how the Open Notebook REST API handles podcast episode retry on failure. Discover how it validates state, cleans files, deletes records, and resubmits jobs.

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

---

**The Open Notebook REST API exposes a `POST /podcasts/episodes/{episode_id}/retry` endpoint that validates the failure state, cleans up orphaned audio files, deletes the old record, and submits a fresh generation job via the surreal-commands system.**

The `lfnovo/open-notebook` repository provides a robust backend for podcast generation that includes dedicated recovery mechanisms for failed episodes. When a podcast generation job fails or encounters an error, the REST API offers a stateless retry mechanism that ensures clean state management before resubmitting the task. Understanding how this podcast episode retry flow works is essential for building reliable client integrations and debugging production failures.

## Retry Endpoint Architecture

The retry functionality is implemented as a stateless HTTP endpoint that orchestrates cleanup and resubmission without relying on in-memory state.

### Route Registration and Validation

The retry route is registered in [`api/routers/podcasts.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/podcasts.py) at lines 15-18:

```python
@router.post("/episodes/{episode_id}/retry")
async def retry_episode(episode_id: str):
    # Implementation handles validation and resubmission

```

When invoked, the handler first retrieves the persisted `PodcastEpisode` object via `PodcastService.get_episode` (defined in [`api/podcast_service.py`](https://github.com/lfnovo/open-notebook/blob/main/api/podcast_service.py) lines 52-57). This ensures the retry operation works against the database record rather than any ephemeral cache.

### State Verification

Before proceeding, the system validates that the episode is actually in a failed state. In [`api/routers/podcasts.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/podcasts.py) lines 21-27, the handler calls `episode.get_job_detail()` and checks that the job status is either `"failed"` or `"error"`. If the episode is not in one of these terminal failure states, the API immediately returns a `400 Bad Request` response, preventing accidental duplicate jobs for episodes that are still processing or completed successfully.

## The Retry Workflow

Once validation passes, the retry mechanism executes a cleanup-and-resubmit pattern that ensures idempotent operations.

### Cleaning Up Failed Artifacts

The handler extracts the original parameters—including profile names, episode name, and source content—from the stored episode record (lines 30-34 in [`api/routers/podcasts.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/podcasts.py)). It then performs critical cleanup operations:

1. **File system cleanup**: If an audio file exists on disk, the code removes it using `Path.unlink()` (lines 41-48 in [`api/routers/podcasts.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/podcasts.py)). This prevents orphaned media files from accumulating in storage.

2. **Database cleanup**: The old episode record itself is deleted via `await episode.delete()` (lines 50-52 in [`api/routers/podcasts.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/podcasts.py)). This ensures the retry starts with a completely fresh slate.

### Resubmitting the Generation Job

After cleanup, the same original parameters are passed to `PodcastService.submit_generation_job` (implemented in [`api/podcast_service.py`](https://github.com/lfnovo/open-notebook/blob/main/api/podcast_service.py) lines 36-45 and 95-102). This method creates a new **surreal-commands** job via the `submit_command` function, which guarantees idempotent background processing and provides status tracking via `get_command_status`. The service returns a fresh `job_id` that the API includes in its response.

## Error Handling and Safety Mechanisms

The retry endpoint implements specific HTTP status codes to provide clear client feedback. If any unexpected exception occurs during the retry process, the handler catches the error in lines 66-69 of [`api/routers/podcasts.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/podcasts.py), logs the failure, and returns a generic `500 Internal Server Error`.

The **safety checks** ensure that only episodes truly marked as failed can be retried, while the **file system cleanup** prevents storage leaks from partial generation attempts. Together, these mechanisms create a robust podcast episode retry system that maintains data integrity.

## Implementation Example

To retry a failed episode from a client application:

```python
import requests

episode_id = "12345"
url = f"http://localhost:5055/api/podcasts/episodes/{episode_id}/retry"
resp = requests.post(url)

if resp.status_code == 200:
    data = resp.json()
    print("Retry submitted, new job ID:", data["job_id"])
else:
    print("Retry failed:", resp.status_code, resp.json())

```

After submitting the retry, poll the new job status using the returned ID:

```python
job_id = "abcd-efgh"
status_url = f"http://localhost:5055/api/podcasts/jobs/{job_id}"
status = requests.get(status_url).json()
print("Job status:", status["status"])

```

## Summary

- **The retry endpoint** (`POST /podcasts/episodes/{episode_id}/retry`) in [`api/routers/podcasts.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/podcasts.py) provides a dedicated route for recovering failed podcast generations.
- **State validation** ensures only episodes with `"failed"` or `"error"` status can be retried, returning `400 Bad Request` for invalid states.
- **Cleanup operations** remove orphaned audio files via `Path.unlink()` and delete the old episode record before creating a new one.
- **Job resubmission** uses `PodcastService.submit_generation_job` to create a fresh surreal-commands background job with a new `job_id`.
- **Error handling** distinguishes between client errors (400) and server failures (500) with appropriate logging.

## Frequently Asked Questions

### What happens if I try to retry a podcast episode that hasn't failed?

The API returns a `400 Bad Request` response. The handler checks `episode.get_job_detail()` and verifies the status is either `"failed"` or `"error"` before proceeding. This prevents accidental duplicate generation jobs for episodes that are still processing or have already completed successfully.

### Does the retry endpoint delete the old audio file before creating a new one?

Yes. The implementation explicitly checks for existing audio files and removes them using `Path.unlink()` (lines 41-48 in [`api/routers/podcasts.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/podcasts.py)). It also deletes the old episode record via `await episode.delete()` before submitting the new generation job, ensuring no orphaned files remain in storage.

### How does the retry mechanism ensure the new job uses the same parameters as the original?

The handler extracts the original profile names, episode name, and content from the stored episode record (lines 30-34 in [`api/routers/podcasts.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/podcasts.py)) before deleting it. These parameters are then passed unchanged to `PodcastService.submit_generation_job`, which creates the new surreal-commands job with identical configuration.

### What error codes does the retry endpoint return?

The endpoint returns `400 Bad Request` if the episode is not in a failed state, `200 OK` with a new `job_id` on successful resubmission, and `500 Internal Server Error` if any unexpected exception occurs during the cleanup or resubmission process.