Files
magnus919_agent-skills/transistor/references/episode-publish-lifecycle.md
Magnus Hedemarkandfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> d1e282a95d docs(transistor): scope publish audio-guard and add missing trigger rows
Document that the publish audio-guard fires only for --status published
(scheduling intentionally precedes audio attach) and that its pre-publish
episode GET consumes one rate-limit slot, relevant to bulk re-publish
loops.

Add references/skill-triggers.md rows for transistor and trakt, the two
thickened skills missing from the trigger index; phrasing follows each
skill frontmatter description.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-08-30 01:52:43 -04:00

8.9 KiB

The Episode Publish Lifecycle (draft → audio → publish)

The single most important behavioral fact of the Transistor API: creating an episode never publishes it, and updating an episode never publishes it. Publishing, scheduling, and unpublishing travel on their own dedicated endpoint. Everything below is from the official reference (developers.transistor.fm, fetched live 2026-08-29).

States

attributes.status is exactly one of:

Status Meaning
draft Not in the RSS feed. New episodes start here (published_at: null).
scheduled Will publish at episode[published_at] (show's time zone).
published Live in the RSS feed; published_at records the publish time.

Transistor's own tooling describes the same model ("Episodes are always created as drafts, and publishing is a separate tool call" — the vendor MCP server), and the REST docs describe the publish endpoint's purpose as "Publish a single episode now or in the past, schedule for the future, or revert to a draft." All three states go through the same endpoint: it is a setter, not a one-way transition — you can pull a published episode back to draft, or re-publish a draft later.

The publish request (exact documented shape)

The endpoint is PATCH /v1/episodes/:id/publish — a metadata PATCH to /v1/episodes/:id will not publish (the docs say so on the update endpoint's own page). The episode ID is the URL path parameter; the body carries the status:

curl https://api.transistor.fm/v1/episodes/<EPISODE_ID>/publish -X PATCH \
  -H "x-api-key: <API_KEY>" \
  -d "episode[status]=published" \
  -d "fields[episode][]=status"
  • Required: episode[status] ∈ {draft, scheduled, published}.
  • Optional: episode[published_at] — the publish date/time in the show's time zone. Combine it with episode[status]=scheduled to schedule for the future, or with published to backdate an episode (e.g. importing an archive with historical dates).
  • The documented request body is form-encoded bracket keys. The API intro says JSON bodies are accepted generally, but the reference publishes no JSON equivalent for this action — copy the documented shape above.
  • Note what the body does not contain: no id, no type, no JSON:API data wrapper. This is not a JSON:API resource-identifier request body; the resource identity lives in the URL. (The response, by contrast, is a standard JSON:API document — see below.)

Documented scheduling example (same endpoint):

curl https://api.transistor.fm/v1/episodes/<EPISODE_ID>/publish -X PATCH \
  -H "x-api-key: <API_KEY>" \
  -d "episode[status]=scheduled" \
  -d "episode[published_at]=2026-09-03 09:00:00"

The publish response

With fields[episode][]=status the documented response is:

{
  "data": {
    "id": "<EPISODE_ID>",
    "type": "episode",
    "attributes": {"status": "published"},
    "relationships": {}
  }
}

Read success off the response: data.id matches the episode you patched, data.type is "episode", and data.attributes.status carries the new state. Without the fieldset you also get published_at, media_url, share_url, duration, and the rest of the episode resource.

Worked recipe: create → attach audio → publish (CLI)

export TRANSISTOR_API_KEY="<API_KEY>"

# 1. Create the episode (always a draft). Audio can ride along now:
transistor episode-create --show <SHOW_ID> \
  --title "Episode 12: Roasting" \
  --summary "A primer on roasting coffee" \
  --season 2 --number 4 \
  --audio-url "https://uploads.example.com/ep12.mp3"
# -> {"id": "<EPISODE_ID>", "status": "draft", ...}

# (skip to 3 if you attached audio above)
# 2. Attach audio later via the metadata PATCH — this does NOT publish:
transistor episode-update --id <EPISODE_ID> \
  --audio-url "https://uploads.example.com/ep12.mp3"

# 3. Publish on the dedicated endpoint:
transistor episode-publish --id <EPISODE_ID>
# PATCH /v1/episodes/<EPISODE_ID>/publish with episode[status]=published

The bundled CLI guards step 3: if attributes.media_url is still empty it refuses to publish (an audio-less item would go out to every feed reader) and prints the attach-audio recipe; --force overrides. The guard is scoped to --status published (the default) — scheduling intentionally precedes audio attach, so a scheduled publish needs no audio yet — and it performs one pre-publish GET /episodes/:id, which consumes one of the 10-requests-per-10-seconds rate-limit slots: worth counting in bulk re-publish loops.

Worked recipe: raw curl

# 1. Create a draft
curl https://api.transistor.fm/v1/episodes -X POST \
  -H "x-api-key: <API_KEY>" \
  -d "episode[show_id]=<SHOW_ID>" \
  -d "episode[title]=Example episode" \
  -d "episode[audio_url]=https://example.com/audio/episode.mp3"
# -> {"data": {"id": "<EPISODE_ID>", "attributes": {"status": "draft", "published_at": null, ...}}}

# 2. (only if audio was omitted) attach through the ordinary metadata PATCH
curl https://api.transistor.fm/v1/episodes/<EPISODE_ID> -X PATCH \
  -H "x-api-key: <API_KEY>" \
  -d "episode[audio_url]=https://example.com/audio/episode.mp3"

# 3. Publish through the dedicated endpoint
curl https://api.transistor.fm/v1/episodes/<EPISODE_ID>/publish -X PATCH \
  -H "x-api-key: <API_KEY>" \
  -d "episode[status]=published"
# -> {"data": {"id": "<EPISODE_ID>", "type": "episode",
#              "attributes": {"status": "published"}, "relationships": {}}}

Local files: the authorize-upload detour

If the audio exists only on disk (no public URL), insert this before step 1 or 2:

# 1. Authorize: get a signed upload URL (max 5GB)
transistor authorize-upload --filename Episode1.mp3
# -> {"audio_url": "https://uploads.example.com/ep1.mp3",
#     "upload_url": "https://...r2.cloudflarestorage.com/...",
#     "content_type": "audio/mpeg", "expires_in": 600}

# 2. PUT the file bytes to attributes.upload_url with the returned
#    Content-Type (the CLI does this when you pass --file):
curl -X PUT -H "Content-Type: audio/mpeg" -T /path/to/Episode1.mp3 "<UPLOAD_URL>"

# 3. Attach attributes.audio_url via episode-create/episode-update, then publish.

The signed URL expires (expires_in, example 600 s) — upload promptly and only then attach. Accepted formats (vendor-documented): .mp3, .m4a, .wav, .aif, .aiff, .aifc, .mp4, .mov.

Gotchas in the lifecycle

  • Publishing is never a side effect. POST /episodes and PATCH /episodes/:id both return status: "draft" (or leave it untouched) no matter what fields you send. If your episode stays stubbornly draft, you are missing the /publish endpoint call — that is the bug, not a permissions problem.
  • published_at vs status. Setting episode[published_at] alone on the metadata PATCH does nothing to feed visibility; the publish endpoint's episode[status] field is what changes state, and published_at only qualifies when.
  • Time zone. episode[published_at] is interpreted in the show's configured time_zone (a show attribute), not in UTC and not in your machine's zone. Check transistor show --id <SHOW_ID> --json.
  • Watch processing. After attaching audio, audio_processing is true until Transistor finishes; processing_failure explains failures. Publishing with an unprocessed/failed file pushes silence to subscribers.
  • Unpublish = status draft. Same endpoint, episode[status]=draft; the episode drops out of the RSS feed but keeps its id, audio, and metadata.
  • Rate budget. The 10 req / 10 s limit applies to the whole lifecycle; a create + audio-attach + publish burst is fine, a loop re-publishing 50 episodes needs sleeps or webhooks.

Sources