nexu-io/open-design · error · Error

template binding must resolve to a scalar: ${binding}

Error message

template binding must resolve to a scalar: ${binding}

What it means

Thrown by scalarOrThrow when a resolved binding value is an Array or a non-null object. Live artifact interpolation only substitutes scalar values (string, number, boolean, null); binding a path that resolves to a structured value would either render '[object Object]' / JSON spam, or invite unsafe serialization, so the render rejects it and asks the author to drill down to a scalar.

Source

Thrown at apps/daemon/src/live-artifacts/render.ts:79

      if (!/^\d+$/.test(segment)) throw new Error(`invalid array segment in template binding path: ${rawPath}`);
      current = current[Number(segment)];
      continue;
    }
    if (typeof current !== 'object') return '';
    current = (current as Record<string, unknown>)[segment];
  }
  return current ?? '';
}

function readTemplatePath(dataJson: BoundedJsonObject, rawPath: string): unknown {
  const segments = rawPath.split('.');
  if (segments.shift() !== 'data') throw new Error(`unsupported template binding path: ${rawPath}`);
  return walkPath(dataJson, segments, rawPath);
}

function scalarOrThrow(value: unknown, binding: string): string {
  if (Array.isArray(value) || (value !== null && typeof value === 'object')) {
    throw new Error(`template binding must resolve to a scalar: ${binding}`);
  }
  return escapeHtmlTemplateValue(value);
}

function rootResolver(dataJson: BoundedJsonObject): BindingResolver {
  return (binding) => {
    if (!TEMPLATE_PATH.test(binding) || !binding.startsWith('data')) {
      throw new Error(`invalid template binding path: ${binding}`);
    }
    return scalarOrThrow(readTemplatePath(dataJson, binding), binding);
  };
}

function childResolver(parent: BindingResolver, varName: string, item: unknown): BindingResolver {
  return (binding) => {
    if (!TEMPLATE_PATH.test(binding)) throw new Error(`invalid template binding path: ${binding}`);
    const segments = binding.split('.');
    if (segments[0] !== varName) return parent(binding);

View on GitHub (pinned to 5be4028344)

Solutions

  1. Drill the path to a scalar leaf: {{data.user.name}} instead of {{data.user}}.
  2. For arrays, iterate with a repeat directive and bind a scalar inside the loop body.
  3. If you must surface structured data, select individual scalar fields in the refresh source rather than dumping the object.

Example fix

// before
<div>{{data.user}}</div>
// after
<div>{{data.user.name}} ({{data.user.email}})</div>
Defensive patterns

Strategy: type-guard

Validate before calling

function isScalarLeaf(v: unknown): v is string | number | boolean | null {
  return v === null || ['string','number','boolean'].includes(typeof v);
}

Type guard

function isScalar(v: unknown): v is string | number | boolean | null {
  return v === null || typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean';
}

Prevention

When it happens

Trigger: {{data.items}} where data.items is an array; {{data.user}} where data.user is an object; {{data.repo}} bound to the full repository object from the GitHub metric instead of a single field like {{data.repo.full_name}}.

Common situations: Model binds a whole sub-object expecting JSON pretty-printing; data shape expanded (a flat field became nested); forgetting to select a leaf field from the refresh source output.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/230b099616342e4f. Report an issue: GitHub.