n8n-io/n8n · error · Error
expr(newCredential('${value.name}')) is invalid. Use newCred
Error message
expr(newCredential('${value.name}')) is invalid. Use newCredential() directly in the credentials config, not inside expr(). What it means
expr() only marks a string as an n8n expression by prepending '='. At runtime the SDK AST interpreter can pass non-string values into it, and when that value looks like a NewCredentialImpl (has __newCredential===true and a string name) this dedicated error fires instead of the generic type error. The message tells you credentials must live in the node's credentials config, never inside expr().
Source
Thrown at packages/@n8n/workflow-sdk/src/expression/index.ts:57
function isNewCredentialLike(value: unknown): value is { __newCredential: true; name: string } {
return (
typeof value === 'object' &&
value !== null &&
'__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_$]*$/;
View on GitHub (pinned to 5ac6606e81)
Solutions
- Move newCredential() out of expr() and into the node's credentials config object, e.g. addField({ credentials: { myApi: newCredential('My Api') } }).
- If you only need the credential name as a string in an expression, pass the literal name string, not the newCredential() result.
- Re-run lintWorkflowSource on the SDK file to confirm no expr(newCredential(...)) patterns remain.
Example fix
// before
const n = telegram.addNode({
parameters: { url: expr(newCredential('myApi')) },
});
// after — credential belongs in credentials config, not in expr()
const n = telegram.addNode({
parameters: { url: expr('{{ $json.webhookUrl }}') },
credentials: { myApi: newCredential('myApi') },
}); Defensive patterns
Strategy: type-guard
Validate before calling
// Before calling expr(), confirm the value is a string and not a credential object.
function isStringLiteral(v: unknown): v is string {
return typeof v === 'string';
}
if (!isStringLiteral(value)) {
// value is a credential/object — do NOT pass to expr(); put it in credentials config.
} Type guard
function isNewCredentialLike(v: unknown): v is { __newCredential: true; name: string } {
return typeof v === 'object' && v !== null && (v as any).__newCredential === true && typeof (v as any).name === 'string';
} Prevention
- Never wrap newCredential() in expr() — credentials belong in the node's credentials config object.
- Run lintWorkflowSource() on generated SDK code to catch expr(newCredential(...)) before deploy.
- Treat expr() as taking only literal expression text containing {{ }} syntax.
When it happens
Trigger: Writing expr(newCredential('My Api')) in SDK builder code, or wrapping a newCredential() call inside a template literal fed to expr() (e.g. expr(`={{ $creds.${newCredential('x')} }}`)). Also triggered when an LLM inlines a credential object as a parameter value through expr().
Common situations: AI-generated workflow code that confuses where credentials go; trying to reference a credential inside an expression string; porting old patterns that put credential names into expression syntax.
Related errors
- expr() requires a string argument, but received ${typeof val
- nodeJson() requires a non-empty JSON path.
- nodeJson() requires a node or node name.
- LangSmithTelemetry creates its own tracer — do not use .otlp
- No suspended run found for runId: ${this.runId}
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/ee91fbc8838286ad.
Report an issue: GitHub.