mastra-ai/mastra · error

Invalid Code Mode external identifier: ${externalName}

Error message

Invalid Code Mode external identifier: ${externalName}

What it means

Code Mode exposes each tool to sandboxed code as a global named external_<sanitizedToolId>. buildRunner validates each sanitized external name against /^[A-Za-z_$][A-Za-z0-9_$]*$/ because it becomes a global property suffix; an invalid identifier would produce unusable code, so buildRunner throws.

Source

Thrown at packages/core/src/tools/code-mode/runner.ts:53

 * Top-level `return`, `await`, and `const` work because the body lives inside
 * an async function.
 */
export function buildProgramModule(program: string): string {
  return `export default async function () {\n${program}\n}\n`;
}

/**
 * Produce the full runner source to write into the sandbox and run with node.
 */
export function buildRunner({ programModule, externals }: BuildRunnerOptions): string {
  // `buildRunner` is exported, so a caller could pass a non-sanitized name.
  // External names become global property suffixes, so reject anything that
  // isn't a legal identifier instead of producing an unusable global.
  const SAFE_IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
  const seen = new Map<string, string>();
  for (const { externalName, toolId } of externals) {
    if (!SAFE_IDENT.test(externalName)) {
      throw new Error(`Invalid Code Mode external identifier: ${externalName}`);
    }
    // Two tool ids can sanitize to the same external name (e.g. `a-b` and
    // `a_b` both become `a_b`). The install loop below would silently overwrite
    // the earlier global, leaving one tool unreachable. Fail fast instead.
    const existing = seen.get(externalName);
    if (existing) {
      throw new Error(
        `Code Mode external identifier collision: tools "${existing}" and "${toolId}" both map to external_${externalName}`,
      );
    }
    seen.set(externalName, toolId);
  }

  // Externals are emitted as JSON data, not interpolated identifiers. The
  // runner installs each `external_<name>` global in a loop using bracket
  // assignment, so no caller-derived string is ever spliced into the generated
  // source as code. This keeps tool ids strictly data, even if `sanitize`
  // changes.

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Rename the tool id so it sanitizes to a legal identifier (start with a letter, $, or _).
  2. If tool ids come from user input, validate them against a safe-identifier pattern at registration time.
  3. Prefix numeric-leading ids (e.g. 'tool-123' instead of '123').

Example fix

// before
createCodeMode({ tools: { '123-weather': weatherTool } });
// after
createCodeMode({ tools: { 'weather123': weatherTool } });
Defensive patterns

Strategy: validation

Validate before calling

const SAFE_IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
for (const id of Object.keys(tools)) {
  if (!SAFE_IDENT.test(sanitizeToolId(id))) {
    throw new Error(`Tool id "${id}" is not Code Mode-safe; rename it`);
  }
}

Type guard

function isCodeModeSafeId(id: string): boolean {
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(id) && id.length > 0;
}

Try / catch

try {
  codeMode = createCodeMode({ tools });
} catch (e) {
  if (String(e.message).includes('Invalid Code Mode external identifier')) {
    // sanitize/rename offending tool ids programmatically
  } else throw e;
}

Prevention

When it happens

Trigger: A tool id sanitizes (via sanitizeToolId) to a string that is not a legal JS identifier, e.g. starts with a digit ('123-tool' -> '123_tool') or is empty/whitespace after sanitization.

Common situations: Tool names beginning with numbers or composed only of punctuation; dynamically generated tool ids from user input; non-ASCII tool ids stripped to an empty string by sanitization.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/741fd8bf368dcb95. Report an issue: GitHub.