Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/rivenmedia/riven-ts/llms.txt

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

Hooks are the primary way plugins react to things happening inside Riven. Each hook is a plain async function that receives a typed event payload, a DataSourceMap, a PluginSettings instance, and a logger — and optionally returns a response value that the Riven core acts on. Under the hood every hook gets its own dedicated BullMQ queue so it runs independently of other system work.

Anatomy of a hook handler

The handler signature is defined by createEventHandlerSchema in the SDK. Every handler receives the same four arguments regardless of event type:
type HookHandler<TEvent, TResponse = void> = (args: {
  event: Omit<TEvent, 'type'>; // the event payload, without the discriminant
  dataSources: DataSourceMap;  // access any DataSource registered by this plugin
  settings: PluginSettings;    // read plugin settings via settings.get(Schema)
  logger: Logger;              // Winston logger scoped to this job
}) => Promise<TResponse>;

Registering hooks in a plugin

Hooks live in the hooks object of your RivenPlugin export. Keys are event type strings; values are handler functions. All keys are optional — only subscribe to the events your plugin needs.
import type { RivenPlugin } from '@repo/util-plugin-sdk';

export const plugin: RivenPlugin = {
  name: pluginConfig.name,
  version: packageJson.version,
  dataSources: [TvdbAPI, TvMazeAPI],
  resolvers: [TvdbResolver, TvdbSettingsResolver],
  settingsSchema: TvdbSettings,

  hooks: {
    'riven.media-item.index.requested.show': indexTVDBMediaItem,
  },

  async validator({ dataSources }) {
    return dataSources.get(TvdbAPI).validate();
  },
};

BullMQ queue per hook

When Riven receives an event it fans it out to all registered plugin handlers. Each plugin’s handler runs inside its own queue named:
${eventType}.plugin[${pluginName}]
For example, the TVDB plugin’s indexing hook runs in the queue:
riven.media-item.index.requested.show.plugin[@repo/plugin-tvdb]
This means a slow or failing hook in one plugin never blocks another plugin’s hook for the same event.

Accessing DataSources inside hooks

Use dataSources.get(YourDataSourceClass) to retrieve an instance of any DataSource your plugin has registered. The DataSourceMap.get() method throws at runtime if the class is not found, catching mismatches early.
import { TvdbAPI } from '../datasource/tvdb.datasource.ts';
import { TvMazeAPI } from '../datasource/tvmaze.datasource.ts';

const myHook = async ({ dataSources, event }) => {
  const tvdb = dataSources.get(TvdbAPI);
  const tvMaze = dataSources.get(TvMazeAPI);
  // ...
};

Real-world example: TVDB indexing hook

The TVDB plugin hooks into riven.media-item.index.requested.show to populate show metadata. If the item has a tvdbId it fetches series and episode data from TVDB and timezone info from TVMaze, then returns a structured item payload for the Riven core to persist.
import { TvdbAPI } from '../datasource/tvdb.datasource.ts';
import { TvMazeAPI } from '../datasource/tvmaze.datasource.ts';
import { transformSeries } from '../transformers/transform-series.ts';

import type { MediaItemIndexRequestedShowEventHandler } from
  '@repo/util-plugin-sdk/schemas/events/media-item.index.requested.event';
import type z from 'zod';

export const indexTVDBMediaItem: z.infer<
  typeof MediaItemIndexRequestedShowEventHandler
> = async ({ dataSources, event }) => {
  if (event.item.tvdbId) {
    const api = dataSources.get(TvdbAPI);
    const tvMazeApi = dataSources.get(TvMazeAPI);

    const series = await api.getSeries(event.item.tvdbId);
    const episodes = await api.getAllEpisodesInOfficialOrder(event.item.tvdbId);
    const timezone = await tvMazeApi.getShowTimezone(event.item.tvdbId);

    return {
      item: transformSeries(event.item, series, episodes, timezone),
    };
  } else if (event.item.imdbId) {
    // IMDb-only path not yet implemented
    return null;
  }

  return null;
};
Use the exported handler type (e.g. z.infer<typeof MediaItemIndexRequestedShowEventHandler>) for the handler variable so TypeScript enforces the exact return shape the core expects.

All available hook events

Lifecycle

Event typeFired whenReturn type
riven.core.startedThe Riven application has fully startedvoid
riven.core.shutdownThe application is shutting down gracefullyvoid

Content service

Event typeFired whenReturn type
riven.content-service.requestedThe core polls for new media items to request{ movies, shows, updateIntervalSeconds }
The riven.content-service.requested hook is how content-source plugins (e.g. a watchlist or Overseerr integration) push new media items into Riven. Return movies and shows arrays with at least one external ID each.

Item request

Event typeFired whenReturn type
riven.item-request.create.successA new item request was created successfullyvoid
riven.item-request.create.errorAn item request creation failedvoid
riven.item-request.create.error.conflictAn item request failed due to a conflict (duplicate)void
riven.item-request.removedAn item request was removedvoid
riven.item-request.update.successAn item request was updatedvoid

Indexing

Event typeFired whenReturn type
riven.media-item.index.requested.movieA movie needs metadata indexed{ item: IndexedMovieItem } | null
riven.media-item.index.requested.showA show needs metadata indexed{ item: IndexedShowItem } | null
riven.media-item.index.successA media item was indexed successfullyvoid
riven.media-item.index.errorIndexing failedvoid
riven.media-item.index.error.incorrect-stateIndexing failed because the item was in an unexpected statevoid
Return null from an index hook to indicate your plugin did not handle the item (e.g. because the required external ID is missing). The core collects results from all plugins and uses the first non-null response.

Scraping

Event typeFired whenReturn type
riven.media-item.scrape.requestedAn indexed item needs torrent streams found{ id, results: Record<infoHash, title> }
riven.media-item.scrape.successScraping completed successfullyvoid
riven.media-item.scrape.errorScraping failedvoid
riven.media-item.scrape.error.incorrect-stateScraping failed because the item was in an unexpected statevoid
riven.media-item.scrape.error.no-streams-foundScraping completed but no streams were foundvoid
The results object maps torrent info hashes (SHA-1) to human-readable titles. Return an empty results map when no streams are found.

Downloading

Event typeFired whenReturn type
riven.media-item.download.requestedA torrent needs to be sent to a debrid service{ success, data: { torrentId, files } } | { success: false, statusCode }
riven.media-item.download.cache-check-requestedThe core checks if a torrent is already cachedvaries
riven.media-item.download.provider-list-requestedA list of available download providers is neededvaries
riven.media-item.download.successDownload completedvoid
riven.media-item.download.partial-successDownload partially completedvoid
riven.media-item.download.errorDownload failedvoid
riven.media-item.download.error.incorrect-stateDownload failed because the item was in an unexpected statevoid

Streaming

Event typeFired whenReturn type
riven.media-item.stream-link.requestedA playable URL is needed for a media entry{ success: true, data: { link, isPermalink, expiresAt? } } | { success: false, statusCode }
riven.media-item.stream-link.health-check.requestedThe core checks if an existing stream link is still validvaries

Subtitles

Event typeFired whenReturn type
riven.media-item.subtitle.requestedSubtitles are needed for a media itemvaries

Hook typing with the SDK event schemas

Each event has a corresponding handler schema exported from the events index. Using these schemas for type inference means TypeScript catches mismatches in the return type:
import type { ContentServiceRequestedEventHandler } from
  '@repo/util-plugin-sdk/schemas/events/content-service-requested.event';
import type z from 'zod';

export const fetchWatchlist: z.infer<
  typeof ContentServiceRequestedEventHandler
> = async ({ dataSources, settings }) => {
  const api = dataSources.get(WatchlistAPI);
  const { updateIntervalSeconds } = settings.get(WatchlistSettings);
  const items = await api.getWatchlist();

  return {
    movies: items.filter((i) => i.type === 'movie').map(toExternalIds),
    shows: items.filter((i) => i.type === 'show').map(toExternalIds),
    updateIntervalSeconds,
  };
};
The type discriminant is used internally by Riven to route the event to the correct queue and handlers. By the time your hook function is called, routing is already done, so the type field is stripped from the payload to keep handler signatures clean. You already know the event type from the key you subscribed to.

Build docs developers (and LLMs) love