Audience: Contributors and coding agents adding ElevenLabs models, voices, or nodes to NodeTool.
TL;DR
- Add the model id to the
MODELSarray inelevenlabs-provider.tsand the@propvaluesarray in the relevant node file. - To add a voice, add one entry to
VOICE_ID_MAPin bothelevenlabs-base.tsandelevenlabs-provider.ts. - To add a node, create
packages/elevenlabs-nodes/src/nodes/<name>.ts, export areadonly NodeClass[], and add it tosrc/index.ts. - Run
npm run build:packages(required — this package loads fromdist/), thennpm run check.
Where things live
| What | Path |
|---|---|
| Provider (TTS, voice list, model list) | packages/runtime/src/providers/elevenlabs-provider.ts |
| Shared voice/key helpers | packages/elevenlabs-nodes/src/elevenlabs-base.ts |
| Node package entry | packages/elevenlabs-nodes/src/index.ts |
| TTS node | packages/elevenlabs-nodes/src/nodes/text-to-speech.ts |
| STT node | packages/elevenlabs-nodes/src/nodes/speech-to-text.ts |
| Realtime TTS node (WebSocket) | packages/elevenlabs-nodes/src/nodes/realtime-tts.ts |
| Realtime STT node (WebSocket) | packages/elevenlabs-nodes/src/nodes/realtime-stt.ts |
| Standard voice picker node | packages/elevenlabs-nodes/src/nodes/standard-voice.ts |
| Provider registration | packages/runtime/src/providers/index.ts line 219 |
| Node tests | packages/elevenlabs-nodes/tests/ |
How ElevenLabs models and nodes are defined
Models
Models are static arrays, not fetched at runtime. Two separate places hold them:
Provider (elevenlabs-provider.ts): the MODELS array drives what getAvailableTTSModels() returns to the unified TTS picker:
const MODELS: Array<{ id: string; name: string }> = [
{ id: "eleven_v3", name: "Eleven v3" },
{ id: "eleven_multilingual_v2", name: "Multilingual v2" },
// …
];
Node (text-to-speech.ts / realtime-tts.ts): each node’s model_id prop carries its own values enum that controls what appears in the workflow UI:
@prop({
type: "enum",
default: "eleven_monolingual_v1",
title: "Model",
values: ["eleven_v3", "eleven_multilingual_v2", "eleven_turbo_v2_5", /* … */]
})
declare model_id: any;
The two lists are independent. Keep them in sync when adding a model.
Voices
Voices are also static. The canonical map lives in two files that mirror each other:
packages/elevenlabs-nodes/src/elevenlabs-base.ts— used by all nodes (voice ID resolution, theStandardVoicenode enum)packages/runtime/src/providers/elevenlabs-provider.ts— used by the provider to surface voices in the unified TTS picker
VOICE_ID_MAP maps display name to voice id. VOICE_NAMES = Object.keys(VOICE_ID_MAP) is passed to StandardVoiceNode’s enum values and to getAvailableTTSModels() voice lists.
Nodes
Each node file exports a readonly NodeClass[] named <CATEGORY>_NODES. src/index.ts spreads them all into ELEVENLABS_NODES and registerElevenLabsNodes() calls registry.register() on each.
The package loads from dist/ (see "main": "dist/index.js" in package.json). TypeScript decorators (@prop) require a build step before any changes take effect.
Add a new model or node
Add a model to an existing node
Step 1. Add the model id to MODELS in packages/runtime/src/providers/elevenlabs-provider.ts:
const MODELS: Array<{ id: string; name: string }> = [
{ id: "eleven_v3", name: "Eleven v3" },
{ id: "eleven_multilingual_v3", name: "Multilingual v3" }, // new
// …
];
Step 2. Add the same id to the values array of the model_id prop in the relevant node(s). For TextToSpeechNode in packages/elevenlabs-nodes/src/nodes/text-to-speech.ts:
@prop({
type: "enum",
default: "eleven_monolingual_v1",
title: "Model",
values: [
"eleven_v3",
"eleven_multilingual_v3", // new
"eleven_multilingual_v2",
// …
]
})
declare model_id: any;
Repeat for realtime-tts.ts if the model supports streaming.
Add a voice
Step 1. Add the entry to VOICE_ID_MAP in packages/elevenlabs-nodes/src/elevenlabs-base.ts:
export const VOICE_ID_MAP: Record<string, string> = {
// existing entries …
Matilda: "XrExE9yKIg1WjnnlVkGX", // new
};
Step 2. Mirror the same entry in packages/runtime/src/providers/elevenlabs-provider.ts:
const VOICE_ID_MAP: Record<string, string> = {
// existing entries …
Matilda: "XrExE9yKIg1WjnnlVkGX", // new
};
VOICE_NAMES derives from Object.keys(VOICE_ID_MAP) in both files, so no other change is needed.
Add a node
Step 1. Create packages/elevenlabs-nodes/src/nodes/<name>.ts. Follow the shape of an existing node — extend BaseNode, set the required static fields, use @prop for inputs, implement process():
import { BaseNode, prop } from "@nodetool-ai/node-sdk";
import type { NodeClass } from "@nodetool-ai/node-sdk";
import { getElevenLabsApiKey } from "../elevenlabs-base.js";
export class SoundEffectNode extends BaseNode {
static readonly nodeType = "elevenlabs.SoundEffect";
static readonly body = "content_card";
static readonly title = "Sound Effect";
static readonly description = "Generate a sound effect from a text prompt.";
static readonly metadataOutputTypes = { output: "audio" };
static readonly requiredSettings = ["ELEVENLABS_API_KEY"];
@prop({ type: "str", default: "", title: "Prompt",
description: "Text description of the sound to generate." })
declare prompt: any;
async process(): Promise<Record<string, unknown>> {
const apiKey = getElevenLabsApiKey(this._secrets);
const prompt = String(this.prompt ?? "");
if (!prompt) throw new Error("Prompt is required");
const response = await fetch("https://api.elevenlabs.io/v1/sound-generation", {
method: "POST",
headers: { "xi-api-key": apiKey, "Content-Type": "application/json" },
body: JSON.stringify({ text: prompt })
});
if (!response.ok) {
throw new Error(`ElevenLabs API error: ${await response.text()}`);
}
const data = Buffer.from(await response.arrayBuffer()).toString("base64");
return { output: { type: "audio", data: `data:audio/mpeg;base64,${data}` } };
}
}
export const SOUND_EFFECT_NODES: readonly NodeClass[] = [SoundEffectNode];
Step 2. Register it in packages/elevenlabs-nodes/src/index.ts:
import { SOUND_EFFECT_NODES } from "./nodes/sound-effect.js";
export const ELEVENLABS_NODES: readonly NodeClass[] = [
...TEXT_TO_SPEECH_NODES,
...SPEECH_TO_TEXT_NODES,
...REALTIME_TTS_NODES,
...REALTIME_STT_NODES,
...STANDARD_VOICE_NODES,
...SOUND_EFFECT_NODES, // new
];
Step 3. Add a test in packages/elevenlabs-nodes/tests/<name>.test.ts and add a registry.has("elevenlabs.SoundEffect") assertion in tests/registration.test.ts.
Verify
# Build first — required because this package loads from dist/
npm run build:packages
# Type check all packages
npm run typecheck
# Run the elevenlabs-nodes test suite
npm run test --workspace=packages/elevenlabs-nodes
# Smoke-test a node in isolation (no workflow needed)
npm run dev:nodetool -- node run elevenlabs.TextToSpeech \
--props '{"text":"hello","voice_id":"9BWtsMINqrJLrRacOk9x"}' \
--no-secrets
# Validate a workflow that uses the node (if you have one)
npm run dev:nodetool -- validate workflow.json
# Full check before committing
npm run check
How past PRs did it
-
00d96654feat(elevenlabs): add v3 model and standard voice node— added"eleven_v3"toTextToSpeechNode’smodel_idenum and introducedStandardVoiceNodeas a new node file. Changed files:src/index.ts,src/nodes/standard-voice.ts,src/nodes/text-to-speech.ts,tests/registration.test.ts,tests/standard-voice.test.ts,tests/text-to-speech.test.ts. That’s the canonical pattern: one file per node, export areadonly NodeClass[], add it toindex.ts, add a test. -
7c7b5157test(elevenlabs): assert enum values via getDeclaredProperties— shows the correct testing pattern:TextToSpeechNode.getDeclaredProperties().find(p => p.name === "model_id")?.options.valuesto assert enum contents, rather than inspectingtoDescriptor().
Contributing
Source and issues: https://github.com/nodetool-ai/nodetool
Community discussion: https://discord.gg/WmQTWZRcYE
Run npm run check (typecheck + lint + tests) and ensure it passes before opening a PR. Two things catch most mistakes before review: npm run build:packages (catches missing exports, wrong import paths) and npm run test --workspace=packages/elevenlabs-nodes (catches registration gaps and API contract regressions).