n8n-io/n8n · error · Error

expr() requires a string argument, but received

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.

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.

Appendix: 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)