n8n-io/n8n · error · Error

expr() requires a string argument, but received ${typeof val

Error message

expr() requires a string argument, but received ${typeof value}.

What it means

expr() requires a string argument. If the value passed is neither a string nor a NewCredentialImpl-like object, this generic error reports the actual typeof it received. It exists because the AST interpreter does not enforce the TypeScript string annotation at runtime.

Source

Thrown at packages/@n8n/workflow-sdk/src/expression/index.ts:61

		'__newCredential' in value &&
		(value as Record<string, unknown>).__newCredential === true &&
		'name' in value &&
		typeof (value as Record<string, unknown>).name === 'string'
	);
}

export function expr(expression: string): string {
	if (typeof expression !== 'string') {
		// At runtime, the AST interpreter may pass non-string values (e.g. NewCredentialImpl objects).
		// TypeScript narrows to `never` here since the param is typed as `string`,
		// so we re-bind as `unknown` to perform runtime type checks.
		const value: unknown = expression;
		if (isNewCredentialLike(value)) {
			throw new Error(
				`expr(newCredential('${value.name}')) is invalid. Use newCredential() directly in the credentials config, not inside expr().`,
			);
		}
		throw new Error(`expr() requires a string argument, but received ${typeof value}.`);
	}
	// Strip any leading '=' to prevent double-equals patterns from LLM output
	const normalized = expression.startsWith('=') ? expression.slice(1) : expression;
	return '=' + normalized;
}

// =============================================================================
// Explicit Node JSON Reference Generator
// =============================================================================

type NodeJsonReference = NodeInstance<string, string, unknown> | string;

const IDENTIFIER_PATH_SEGMENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;

function resolveNodeName(node: NodeJsonReference): string {
	return typeof node === 'string' ? node : node.name;
}

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Coerce the value to a string before calling expr(), e.g. expr(`${$json.count}`) or expr(String(value)).
  2. Confirm the upstream field is actually a string; if it can be undefined, default it first.
  3. Use expr() only for literal expression text containing {{ }} syntax, not for arbitrary data.

Example fix

// before
const url = expr($json.baseUrl);   // $json.baseUrl may be undefined/non-string
// after
const url = expr(`={{ $json.baseUrl }}`);  // pass a literal expression string
Defensive patterns

Strategy: type-guard

Validate before calling

// Coerce or reject non-strings before expr().
function asExprString(v: unknown): string {
  if (typeof v !== 'string') throw new TypeError(`expr() needs a string, got ${typeof v}`);
  return v;
}
const out = expr(asExprString(maybeValue));

Type guard

function isString(v: unknown): v is string { return typeof v === 'string'; }

Prevention

When it happens

Trigger: Calling expr(123), expr(someObject), expr(undefined), expr($json.count) where $json.count is a number, or expr(true). Also expr() with no argument.

Common situations: Passing numeric/boolean/object workflow data into expr(); LLM forgetting to interpolate a value into a string; referencing a field whose runtime type is not string.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/95d883224cbad418. Report an issue: GitHub.