n8n-io/n8n · error
SDK_PLACEHOLDER_WRAPPED
SDK_PLACEHOLDER_WRAPPED
Error message
Do not wrap placeholder() in expr(). Use placeholder('hint') directly as the parameter value. What it means
The linter rejects direct calls to `placeholder(...)` that appear as an argument to `expr(...)`. `placeholder('hint')` is itself an expression marker for an unset value the user must fill in; wrapping it in `expr()` defeats its purpose — the parameter should receive `placeholder('hint')` directly. The walker detects a `CallExpression` arg whose callee is `placeholder` inside an `expr()` call and emits `SDK_PLACEHOLDER_WRAPPED` at workflow-sdk-lint.ts:303-313.
Source
Thrown at packages/@n8n/workflow-sdk/src/lint/sdk/workflow-sdk-lint.ts:306
lintIssue({
code: 'SDK_FORBIDDEN_CONSTRUCT',
message:
`'.${method}()' is not available on SDK builder objects. Build strings with template ` +
'literals, or do transforms in a Code node / expr().',
...locationOf(call),
lintTarget: 'sdk',
}),
);
}
}
if (isExprCall(call)) {
for (const arg of call.arguments) {
if (arg.type === 'SpreadElement') continue;
if (arg.type === 'CallExpression' && isPlaceholderCall(arg)) {
issues.push(
lintIssue({
code: 'SDK_PLACEHOLDER_WRAPPED',
message:
"Do not wrap placeholder() in expr(). Use placeholder('hint') directly as the parameter value.",
...locationOf(call),
lintTarget: 'sdk',
}),
);
}
if (arg.type === 'TemplateLiteral') {
for (const expr of arg.expressions) {
if (expr.type === 'CallExpression' && isPlaceholderCall(expr)) {
issues.push(
lintIssue({
code: 'SDK_PLACEHOLDER_WRAPPED',
message:
'Do not embed placeholder() inside expr()/template strings. Use placeholder() as the direct parameter value.',
...locationOf(call),
lintTarget: 'sdk',
}),View on GitHub (pinned to 5ac6606e81)
Solutions
- Use `placeholder('hint')` directly as the parameter value — do not wrap it in `expr()`.
- If the value needs runtime computation, use `expr()` with a real expression body referencing `$json` / `$now` / `$('Node')` — not `placeholder()`.
- Audit every `expr(...)` argument and ensure none is a direct `placeholder(...)` call.
Example fix
// before
addNode('http', { url: expr(placeholder('apiUrl')) });
// after — placeholder() is the direct parameter value
addNode('http', { url: placeholder('apiUrl') }); Defensive patterns
Strategy: validation
Validate before calling
import { parse } from 'acorn';
function placeholderWrappedInExpr(builderSource: string): boolean {
const ast = parse(builderSource, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
let found = false;
walk(ast, (n: any) => {
if (n.type !== 'CallExpression') return;
if (n.callee.type !== 'Identifier' || n.callee.name !== 'expr') return;
for (const arg of n.arguments) {
if (arg.type === 'SpreadElement') continue;
if (arg.type === 'CallExpression' && arg.callee.type === 'Identifier' && arg.callee.name === 'placeholder') {
found = true;
}
}
});
return found;
} Type guard
import type { Node, CallExpression } from 'estree';
const isPlaceholderCall = (n: Node): n is CallExpression =>
n.type === 'CallExpression' && n.callee.type === 'Identifier' && n.callee.name === 'placeholder';
const isExprCall = (n: Node): n is CallExpression =>
n.type === 'CallExpression' && n.callee.type === 'Identifier' && n.callee.name === 'expr'; Prevention
- Use `placeholder('hint')` as the direct parameter value — never wrap it in `expr()`.
- Audit every `expr(...)` argument; if it contains `placeholder(...)`, unwrap it.
- Lint generated builder source with `lintWorkflowSdkSource`.
When it happens
Trigger: Writing `expr(placeholder('apiKey'))`, `expr(`prefix-${placeholder('x')}`)` (the template-literal case is a sibling check), or otherwise passing a direct `placeholder()` call as an argument to `expr()`.
Common situations: Agents wrapping every dynamic value in `expr()` defensively. Confusion about whether `placeholder()` is a runtime expression or a parameter marker. Copy-paste from an example that used `expr()` for a real lookup.
Related errors
- SDK_UNSOLICITED_STICKY
- Thread ${threadId}: ${buildSignalIds.size} workflow(s) were
- Unsupported syntax: '${nodeType}' is not allowed in SDK code
- '${node.kind}' declarations are not allowed. Use 'const' onl
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/1b55bee77abcba24.
Report an issue: GitHub.