SDK Reference

The Overseer SDK lets extensions send and receive events, read shared datasets, and show notifications. Import from @overseer-studio/sdk in any bundled extension — all functions are available as named exports.

Getting started

Use onReady to run code once the extension has loaded and configuration is available.

import { onReady } from '@overseer-studio/sdk';

onReady((event) => {
  const { extensionId, config, language } = event.detail;

  initialize(config);
});

onReady returns an unsubscribe function if you need to clean up.

Methods

import { sendEvent, subscribe, unsubscribe } from '@overseer-studio/sdk';

sendEvent(name, payload?)

Broadcasts an event to all extensions subscribed to name.

ParameterTypeRequiredDescription
namestringYesThe event name to broadcast.
payloadanyNoData to include with the event.

The event name must be listed in the extension manifest’s permissions.send array.

sendEvent('dice-result', {
  equation: '2d6+3',
  rolls: [4, 2],
  total: 9,
});

subscribe(name, callback)

Listens for events broadcast by other extensions.

ParameterTypeRequiredDescription
namestringYesThe event name to listen for.
callbackfunctionYesCalled with the event payload when a matching event arrives. The callback receives the exact payload passed to sendEvent() by the sending extension — no additional metadata is included.

The event name must be listed in the extension manifest’s permissions.receive array.

subscribe('dice-roll', (payload) => {
  console.log('Roll requested:', payload.equation);
});

unsubscribe(name)

Stops listening for a previously subscribed event.

ParameterTypeRequiredDescription
namestringYesThe event name to stop listening for.
unsubscribe('dice-roll');

Lifecycle events

import { onReady, onConfigChanged } from '@overseer-studio/sdk';

Both functions return an unsubscribe function.

onReady(callback)

Called once when the extension loads and the SDK is available.

PropertyTypeDescription
event.detail.extensionIdstringThis extension’s unique identifier.
event.detail.configobjectCurrent configuration values. Keys match the field names in the manifest’s config object.
event.detail.languagestringThe user’s current locale (e.g., "en_US").
event.detail.stateobject | nullPersisted runtime state for this tile, or null if nothing has been saved. See the State API.
const cleanup = onReady((event) => {
  const { extensionId, config, language, state } = event.detail;
  initialize(config, state);
});

onConfigChanged(callback)

Called when the user changes a configuration value in the tile editor.

PropertyTypeDescription
event.detail.extensionIdstringThis extension’s unique identifier.
event.detail.configobjectThe updated configuration values.
event.detail.languagestringThe user’s current locale.
onConfigChanged((event) => {
  const { config } = event.detail;
  applyNewConfig(config);
});

Data API

Extensions can read shared datasets through the SDK’s data functions. Datasets are declared by plugins and referenced by a dataset ID in the format @pluginId:localId. See Datasets for how they’re provided and Data Providers for the full model.

List the dataset IDs your extension reads in the manifest’s consumes array so Overseer can warn users about missing dependencies.

import {
  listDatasets,
  getAllData,
  getDataById,
  createData,
  updateData,
  deleteData,
  callDataDynamic,
  invalidateData,
  onDataChanged,
} from '@overseer-studio/sdk';

Listing datasets

listDatasets()

Returns every dataset currently registered across all loaded plugins.

const datasets = await listDatasets();
// [{ id: '@me/dnd5e-srd:monsters', pluginId: '@me/dnd5e-srd', type: 'static', ... }]

Each entry contains id, pluginId, pluginPath, manifest, and type ('static' or 'dynamic').

Static datasets

Static datasets are backed by a JSON array file shipped with the plugin. Extensions can read, add, edit, and delete records; user edits are merged on top of the bundled records.

getAllData(datasetId, options?)

Reads a paginated page of records.

ParameterTypeRequiredDescription
datasetIdstringYesThe dataset ID (@pluginId:localId).
options.pagenumberNo1-based page number. Default 1.
options.limitnumberNoRecords per page. Default 50, max 200.

Returns { items, total, page, limit }.

const { items, total } = await getAllData('@me/dnd5e-srd:monsters', {
  page: 1,
  limit: 20,
});

getDataById(datasetId, id)

Reads a single record by its ID. Returns null if no record matches. The ID field is whatever the dataset manifest declared as idField (default: id).

const goblin = await getDataById('@me/dnd5e-srd:monsters', 'goblin');

createData(datasetId, record)

Adds a new record. If the record’s ID collides with a bundled record, the new record wins on read.

const monster = await createData('@me/dnd5e-srd:monsters', {
  id: 'homebrew-lich',
  name: 'Homebrew Lich',
  cr: 21,
});

updateData(datasetId, id, updates)

Partially updates an existing record. Returns the updated record.

await updateData('@me/dnd5e-srd:monsters', 'goblin', { hp: 10 });

deleteData(datasetId, id)

Removes a user-created or user-edited record. Deleting a record that overrides a bundled record reverts the bundled version on the next read.

await deleteData('@me/dnd5e-srd:monsters', 'homebrew-lich');

Dynamic datasets

Dynamic datasets are named URL templates that resolve to remote API calls. The store fetches on the extension’s behalf — this bypasses CORS — and returns the raw parsed JSON.

callDataDynamic(datasetId, action, params?)

Invokes an action by name. params are substituted into the URL template’s {paramName} placeholders and URL-encoded.

const results = await callDataDynamic(
  '@me/dnd5e-srd:monsters-live',
  'search',
  { q: 'goblin', cr_max: 1 },
);

No normalization is performed — the response is whatever the remote API returned.

invalidateData(datasetId)

Signals consumers that the dataset’s remote state changed. Fires a data:changed event with type: 'invalidated'.

await callDataDynamic('@me/campaign:notes', 'submit', { note });
await invalidateData('@me/campaign:notes');

Change events

onDataChanged(datasetId, callback)

Subscribes to changes on a specific dataset. Returns an unsubscribe function.

ParameterTypeRequiredDescription
datasetIdstringYesThe dataset ID to listen to.
callbackfunctionYesCalled with a change event when the dataset changes.

The callback receives { datasetId, type, record? }, where type is 'created', 'updated', 'deleted', or 'invalidated'. record is present for CRUD events and absent for invalidated.

const unsubscribe = onDataChanged('@me/dnd5e-srd:monsters', (event) => {
  if (event.type === 'created') console.log('New monster:', event.record);
  if (event.type === 'invalidated') refetch();
});

// Later:
unsubscribe();

State API

Extensions can persist runtime state across sessions — selections, scroll positions, the current page of a multi-page tool. Each tile has its own keyed store; values written by one tile are not visible to other tiles, even instances of the same extension.

State is delivered to the extension on load via onReady’s event.detail.state, and is read or written at any time through getState and setState.

State is saved into the user’s session JSON, alongside the tile that owns it. Sharing a session file shares the state with it. Values must be JSON-serializable.

import { getState, setState } from '@overseer-studio/sdk';

No manifest declaration is required — the State API is available to every extension by default. State changes are not part of the user’s undo history.

getState<T>(key)

Reads a value previously written with setState. Returns null if no value has been set for that key (or if it was explicitly cleared).

ParameterTypeRequiredDescription
keystringYesThe key to read.
type SelectedMonster = { id: string; name: string };

const monster = await getState<SelectedMonster>('selected');
if (monster) {
  highlight(monster.id);
}

For best startup performance, prefer reading initial state from event.detail.state inside onReady rather than calling getState on load — the snapshot is delivered with the ready event, so no round-trip is needed.

setState<T>(key, value)

Writes a value to the tile’s state store. Returns immediately; persistence happens asynchronously. Pass null to clear a key.

ParameterTypeRequiredDescription
keystringYesThe key to write.
valueTYesThe value to store. Must be JSON-serializable. Pass null to clear the key.
type SelectedMonster = { id: string; name: string };

setState<SelectedMonster>('selected', { id: 'goblin', name: 'Goblin' });

// Later, clear the selection:
setState('selected', null);

Each setState call replaces the value at key; other keys in the tile’s state are preserved.

How the event system works

Events sent via sendEvent() are delivered to every extension that has subscribed to that event name. Extensions are isolated from each other — the event system is the only way they communicate.

This enables powerful automation across a GM screen. A character sheet extension can send dice roll equations, a dice roller can pick them up and return results, and a combat log can record everything — each in its own tile, working together through events.

Event names are freeform strings. There’s no global registry — extensions agree on event names by convention. If you’re building extensions that work together, document the events they exchange so other developers can integrate with them.

Complete example

This example shows a TypeScript character sheet extension that sends dice roll requests and displays results. A companion dice roller extension (not shown) would subscribe to dice-roll and send back dice-result events.

Manifest:

{
  "id": "@myname/character-sheet",
  "version": "1.0.0",
  "category": "Character Sheets",
  "label": "Character Sheet",
  "description": "Interactive character sheet with dice integration.",
  "icon": { "$ref": "assets/icon.png" },
  "source": { "$ref": "dist/index.html" },
  "config": {
    "characterName": {
      "type": "string",
      "required": true,
      "label": "Character Name",
      "placeholder": "Tordek the Brave"
    }
  },
  "permissions": {
    "send": ["dice-roll"],
    "receive": ["dice-result"]
  }
}

src/index.ts:

import { onReady, onConfigChanged, sendEvent, subscribe } from '@overseer-studio/sdk';

const nameEl = document.getElementById('name') as HTMLElement;
const logEl = document.getElementById('log') as HTMLElement;

onReady((event) => {
  const { config } = event.detail;
  nameEl.textContent = config.characterName || 'Character Sheet';

  subscribe<{ source: string; total: number; equation: string }>('dice-result', (payload) => {
    const entry = document.createElement('div');
    entry.textContent = `${payload.source}: rolled ${payload.total} (${payload.equation})`;
    logEl.prepend(entry);
  });
});

onConfigChanged((event) => {
  const { config } = event.detail;
  nameEl.textContent = config.characterName || 'Character Sheet';
});

(window as any).rollAbility = (name: string, modifier: number) => {
  const equation = modifier >= 0 ? `1d20+${modifier}` : `1d20${modifier}`;
  sendEvent('dice-roll', { equation, source: name });
};

dist/index.html (loads the compiled bundle):

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    body { font-family: sans-serif; padding: 16px; color: #e0e0e0; background: #1a1a2e; }
    h2 { margin-top: 0; }
    .stat { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #333; }
    .stat button { background: #e63946; color: white; border: none; padding: 4px 12px; border-radius: 4px; cursor: pointer; }
    #log { margin-top: 16px; font-size: 0.9rem; color: #aaa; }
    #log div { padding: 2px 0; }
  </style>
</head>
<body>
  <h2 id="name">Character Sheet</h2>

  <div class="stat">
    <span>Strength (16)</span>
    <button onclick="rollAbility('Strength', 3)">+3</button>
  </div>
  <div class="stat">
    <span>Dexterity (12)</span>
    <button onclick="rollAbility('Dexterity', 1)">+1</button>
  </div>

  <div id="log"></div>

  <script src="bundle.js"></script>
</body>
</html>

Toast notifications

import { toast } from '@overseer-studio/sdk';

toast.success('Done!', 'Your action was successful.');
toast.error('Oops', 'Something went wrong.');
toast.warning('Heads up', 'Check this out.');
toast.info('FYI', 'Here is some information.');

No permission declaration is required for toasts — any extension can send them.

Error handling

If you call sendEvent() with an event name that isn’t listed in your manifest’s permissions.send, the call is silently ignored. The same applies to subscribe() with an event not in permissions.receive.

Debugging

Use console.log in your extension code to trace event flow. You can also build a simple debug extension that subscribes to the events you want to monitor and displays them in the tile — this is a practical way to verify that events are being sent and received as expected.