Skip to content

Inkweaver SDK


Scripts imported by your plugin.json’s main script share a single, global state object. Expressions in the screenplay have access to this state as well.

Plugins, on the other hand, each have their own private state. Anything a plugin wants to expose will be available through its selectors (see getSelectors).

Your state is a plain object. There are no special variable declarations or serialization constraints to worry about. Just store what you need, and Inkweaver manages it for you when a bookmark is created.

Returns the current novel state.

getState<T>(): T
Example
const trust = getState().trust;

Replaces the current novel state with a new value.

setState<T>(newState: T | ((currentState: T) => T)): void

You can pass a new state object directly…

Example
setState({
trust: 1,
achievements: ["first-blood"],
});

…or a function that receives the current state and returns the new one. The functional form is recommended when your update depends on the current value:

Example
setState((current) => ({
...current,
trust: current.trust + 1,
}));

Updates the current novel state by deeply merging the provided value into the existing state.

updateState<T>(newState: T | ((currentState: T) => T)): void

Like setState, you can pass a new value directly or a function that receives the current state and returns the new one:

Example
// Update a single value without affecting the rest of state:
updateState({ trust: 8 });
// Use the functional form when your update depends on the current value:
updateState((current) => ({ trust: current.trust + 1 }));

Returns a collection of functions that plugins use to expose their state to your scripts. Rather than accessing plugin internals directly, selectors give you a safe, managed way to read derived or private state.

getSelectors(): Record<string, Function>

Selectors work naturally with onChange for reactive updates:

Example
const { currentScene } = getSelectors();
onChange(currentScene, (scene) =>
console.log(`Scene changed to ${scene.text}`),
);

See the plugins documentation for a comprehensive list of core plugins, and their selectors.


Event handlers are how your script reacts to what’s happening in the novel. Use onChange to respond when state values change, and onLabel to intercept and control lines in the screenplay as they’re encountered. Together with state, these two functions are the primary tools for building dynamic, reactive stories.

Registers a reactive callback that is triggered whenever a tracked value changes.

onChange<T>(
selector: () => T,
onUpdate: (currentValue: T) => void,
onCleanup?: (lastValue: T) => void
): void

selector is a function that returns the value you want to watch. Whenever any application state changes, the value returned by selector is compared to the last value that it returned. If the value changed, onCleanup is called with the old value, then onUpdate is called with the new one.

Example
onChange(
() => getState().trust,
(trust) => console.log(`Trust is now ${trust}`),
(lastTrust) => console.log(`Trust was ${lastTrust}`),
);

Registers a callback that is triggered whenever a matching label is encountered in the screenplay.

onLabel<T extends BlockLexia>(
pattern: string | RegExp,
callback: (lexia: T, groups: Record<string, string> | undefined) => Promise<undefined | T | boolean>
): void

The pattern can be a string for exact matches, or a regular expression for parametric labels. The callback receives two arguments: the current lexia, and any named capture groups from the regex (or undefined if the pattern is a plain string).

The callback may be asynchronous. The engine will await its return value before deciding how to handle the line.

The callback can return:

  • true or undefined — display the line normally
  • false — skip the line entirely
  • A new BlockLexia of the same type — replace the line with the returned value
Example
/*
* Screenplay Usage:
*
* SOPHIA <if-trustworthy>
* I have something important to say...
*/
onLabel("if-trustworthy", (lexia) => {
const { trust } = getState();
return trust >= 5;
});

Named capture groups make labels parametric. In this example, the required trust range is defined in the screenplay rather than the script:

Example
/*
* Screenplay Usage:
*
* SOPHIA
* I'm not sure I can share this with you... <if-trustworthy 0-3>
* I have something important to say... <if-trustworthy 4-10>
*/
onLabel(
/if-trustworthy (?<from>[0-9]+)-(?<to>[0-9]+)/,
(lexia, { from, to }) => {
const { trust } = getState();
return trust >= parseInt(from) && trust <= parseInt(to);
},
);

These functions control the reader’s position in the story.

Advances the novel by one step. This does nothing if the reader is at the tip of the state history.

stepForward(): Promise<void>
Example
await stepForward();

Steps the novel backward by one step, restoring the previous state.

stepBack(): Promise<void>
Example
await stepBack();

Returns true if the reader is currently stepping backward through the story.

isRewinding(): boolean
Example
const rewinding = isRewinding();

Bookmarks are Inkweaver’s save system. Each bookmark captures a snapshot of the novel’s state at a point in time, allowing the reader to return to it later.

interface Bookmark {
id: string;
image: string; // A base64-encoded image used as the bookmark's thumbnail
time: number;
type: "auto" | "quick" | "user";
}

Returns a list of all saved bookmarks for the current novel.

getBookmarks(): Promise<Array<Bookmark>>
Example
const bookmarks = await getBookmarks();

Saves a bookmark, capturing the current state of the novel.

saveBookmark(bookmark: Bookmark): Promise<void>
Example
await saveBookmark({
id: "Act III",
image: base64EncodedImage,
time: Date.now(),
type: "auto",
});

Loads a previously saved bookmark, restoring the novel to the state captured at that point.

loadBookmark(bookmark: Bookmark): Promise<void>
Example
const bookmarks = await getBookmarks();
await loadBookmark(bookmarks[0]);

These functions handle asset resolution and file I/O across platforms. Use resolve to map logical asset paths to their build-time filenames, and readFile / writeFile for persistent storage outside of Inkweaver’s state system.

Resolves a logical asset path to its physical, build-time filename using the asset manifest.

resolve<T extends RequestInfo | URL>(url: T, locale?: string): T

When Inkweaver builds your novel, assets are saved using content-addressed filenames. resolve uses an asset manifest to find the physical file that corresponds to the logical path you provide.

For localized assets, see the localization guide.

Example
const url = resolve("/assets/shop.json");
const response = await fetch(url);

Reads a file from the platform’s filesystem.

readFile(input: RequestInfo | URL, init?: RequestInit): Promise<Response>

The underlying storage mechanism depends on the build target defined in your project.json:

  • Web — data is persisted to IndexedDB.
  • Desktop — reads from a sandboxed OS-specific application data folder at <appDataPath>/<appName>/files.
Example
const response = await readFile("/notes.txt");
const text = await response.text();

Writes a file to the platform’s filesystem.

writeFile(input: RequestInfo | URL, data: any, init?: RequestInit): Promise<Response>

Uses the same platform-specific storage as readFile.

Example
await writeFile("/notes.txt", "The butler did it.");

Runs an async function and calls onError if the novel state changed during its execution.

immutable<T>(
fn: () => Promise<T>,
onError: (changes: string) => void
): Promise<T>

This is a development-time guard against unintended state changes. If fn causes any state changes, onError is called with a description of what changed.

await immutable(
() => doSomeWork(),
(changes) => console.warn(`Unexpected state changes: ${changes}`),
);

These functions let you extend the engine’s command and selector systems. Commands are how the engine processes updates, and selectors are how plugins expose their state. Through decoration, your scripts can wrap either one to modify or extend behavior.

Any code you write has the same power and privileges as the engine’s built-in systems.

Defines middleware that wraps a system command. The decorator receives the original arguments and a next function that calls the previous implementation.

decorateCommand<TName extends keyof Commands>(
commandName: TName,
fn: (context: Parameters<Commands[TName]>[0], next: Commands[TName]) => ReturnType<Commands[TName]>,
priority?: number
): void

Decorators are applied in priority order — higher numbers run first. If no priority is supplied, 0 is assumed. As a guideline:

  • > 0 — runs before the engine’s built-in behavior
  • < 0 — runs after the engine’s built-in behavior
  • Number.MAX_VALUE / Number.MIN_VALUE — for decorators that must run absolutely first or last
Example
decorateCommand("init", (context, next) => {
console.log("Before init");
const result = next(context);
console.log("After init");
return result;
});

Defines middleware that wraps a selector. Works the same way as decorateCommand, but for selectors.

decorateSelector<TName extends keyof Selectors>(
name: TName,
fn: (context: Parameters<Selectors[TName]>[0], next: Selectors[TName]) => ReturnType<Selectors[TName]>,
priority?: number
): void
Example
decorateSelector("currentScene", (context, next) => {
const scene = next(context);
return { ...scene, customData: getState().extraSceneInfo };
});

These functions are the building blocks that core plugins use to process screenplay content. You won’t need them in a typical script. But if you’re decorating commands to customize how the engine handles text in the screenplay, this is how it’s done under the hood.

Returns a collection of system commands that control the engine’s behavior. Commands are the primary way that the engine processes updates, and plugins can decorate them to extend or modify behavior.

getCommands(): Commands
Example
const { processDialogue } = getCommands();

See the plugins documentation for a comprehensive list of core plugins, and their commands.

Flushes the current state to the engine’s state history.

commitState(): Promise<void>

Inkweaver tracks state changes over time to support stepping forward and backward. commitState creates a checkpoint in this history. Under normal operation, the engine handles this automatically. You only need to call this if you are building a custom command that modifies state and needs to ensure those changes are captured as a discrete step.

Example
await commitState();

Invokes any onLabel callbacks that match the provided lexia.

processLabel<T extends BlockLexia>(lexia: T): Promise<undefined | T | boolean>
Example
processLabel(lexia);

evaluateExpression advanced

Section titled “evaluateExpression ”

Evaluates an expression block in the screenplay.

evaluateExpression(expression: Expression): any
Example
dialogueLexia.expressions.forEach((expression) => {
console.log(evaluateExpression(expression));
});