Runs: cloud. Needs:
ELEVENLABS_API_KEY. The provider constructor throwsELEVENLABS_API_KEY is requiredwithout it. The realtime nodes throwELEVENLABS_API_KEY is not configured.
Adding a model id takes two edits, one in the provider and one in the node enum:
// packages/runtime/src/providers/elevenlabs-provider.ts
const MODELS: Array<{ id: string; name: string }> = [
{ id: "eleven_v3", name: "Eleven v3" },
// …
];
// packages/elevenlabs-nodes/src/nodes/text-to-speech.ts: @prop model_id values: ["eleven_v3", …]
- 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 test:affected,npm run typecheck, andnpm run lint.
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 (registerBuiltinProvider(PROVIDER_IDS.ELEVENLABS, ...)) |
| 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 and not identical. The node’s model_id enum also carries the speech-to-speech models eleven_multilingual_sts_v2 and eleven_english_sts_v2, which the provider omits. Keep them in sync when adding a TTS model.
Voices
Voices are also static. The canonical map lives in two files that mirror each other:
packages/elevenlabs-nodes/src/elevenlabs-base.ts: voice ID resolution and theStandardVoicenode enum.packages/runtime/src/providers/elevenlabs-provider.ts: 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. The TextToSpeech node’s voice_id prop is a plain string that defaults to the Aria id, so it accepts any ElevenLabs voice id. The provider resolves a voice name through the map and passes an unknown value through as an id.
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
Nodes do not call the ElevenLabs API directly. They call callElevenLabs(context, …) from elevenlabs-base.ts, which runs context.callProviderEndpoint through the provider. The provider’s callEndpoint accepts two routes, "text-to-speech" and "speech-to-text", and throws for any other. A node for a different ElevenLabs endpoint needs a new route in callEndpoint first.
Step 1. Create packages/elevenlabs-nodes/src/nodes/<name>.ts. This node reuses the text-to-speech route with a fixed model:
import { BaseNode, isRecord, prop } from "@nodetool-ai/node-sdk";
import type { NodeClass } from "@nodetool-ai/node-sdk";
import { callElevenLabs, VOICE_ID_MAP } from "../elevenlabs-base.js";
export class QuickVoiceNode extends BaseNode {
static readonly nodeType = "elevenlabs.QuickVoice";
static readonly title = "Quick Voice";
static readonly description = "Speak text with Flash v2.5.\nelevenlabs, tts";
static readonly metadataOutputTypes = { output: "audio" };
static readonly requiredSettings = ["ELEVENLABS_API_KEY"];
@prop({ type: "str", default: "Hello", title: "Text" })
declare text: any;
async process(
context?: Parameters<BaseNode["process"]>[0]
): Promise<Record<string, unknown>> {
const text = String(this.text ?? "");
if (!text) throw new Error("Text is required");
const { output } = await callElevenLabs(context, {
capability: "text_to_speech",
model: "eleven_flash_v2_5",
route: "text-to-speech",
input: { voice_id: VOICE_ID_MAP.Aria, text },
nodeId: this.__node_id
});
const audio = isRecord(output) ? output.data : undefined;
if (!(audio instanceof Uint8Array)) {
throw new Error("ElevenLabs returned no audio");
}
const base64 = Buffer.from(audio).toString("base64");
return { output: { type: "audio", data: `data:audio/mpeg;base64,${base64}` } };
}
}
export const QUICK_VOICE_NODES: readonly NodeClass[] = [QuickVoiceNode];
Step 2. Register it in packages/elevenlabs-nodes/src/index.ts:
import { QUICK_VOICE_NODES } from "./nodes/quick-voice.js";
export const ELEVENLABS_NODES: readonly NodeClass[] = [
// existing spreads …
...QUICK_VOICE_NODES, // new
];
Step 3. Add a test in packages/elevenlabs-nodes/tests/<name>.test.ts and a registry.has("elevenlabs.QuickVoice") 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
# Affected tests and the harness gate before committing
npm run test:affected
npm run lint
npm run dev:nodetool -- harness gate --base origin/main
The tests/ files assert enum contents through getDeclaredProperties(), for example TextToSpeechNode.getDeclaredProperties().find(p => p.name === "model_id"). Follow that pattern in new tests.