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

  1. Use `placeholder('hint')` directly as the parameter value — do not wrap it in `expr()`.
  2. If the value needs runtime computation, use `expr()` with a real expression body referencing `$json` / `$now` / `$('Node')` — not `placeholder()`.
  3. 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

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


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