n8n-io/n8n · error · Error
expr(newCredential(' ')) is invalid. Use newCredential()…
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().
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
- nodeJson() requires a node or node name.
- nodeJson() requires a non-empty JSON path.
- 0
- 400
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/ee91fbc8838286ad.
Report an issue: GitHub.
Appendix: 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)