nexu-io/open-design · error

connector refresh output must be a JSON object

Error message

connector refresh output must be a JSON object

What it means

The connector-tool refresh path in executeRefreshSource (refresh-service.ts:97-98) calls connectorService.execute and requires its returned `output` to be a plain JSON object: not null, not a primitive (string/number/boolean), and not an array. The live-artifact refresh merges this object into the document's dataJson via deepMergeBoundedJsonObject, which only works on a record shape. A non-object output means the connector tool's contract does not match what the refresh pipeline expects.

Source

Thrown at apps/daemon/src/live-artifacts/refresh-service.ts:98

  projectId: string;
  source: LiveArtifactSource;
  signal: AbortSignal;
}): Promise<BoundedJsonObject> {
  const { projectsRoot, projectId, source, signal } = options;
  if (source.type === 'connector_tool') {
    const connector = source.connector;
    if (connector === undefined) throw new Error('connector refresh source requires connector metadata');
    const result = await connectorService.execute(
      {
        connectorId: connector.connectorId,
        toolName: connector.toolName,
        input: source.input,
        ...(connector.accountLabel === undefined ? {} : { expectedAccountLabel: connector.accountLabel }),
      },
      { projectsRoot, projectId, purpose: 'artifact_refresh', signal },
    );
    if (result.output === null || typeof result.output !== 'object' || Array.isArray(result.output)) {
      throw new Error('connector refresh output must be a JSON object');
    }
    return result.output;
  }
  if (source.type !== 'daemon_tool' && source.type !== 'local_file') {
    throw new Error(`refresh source ${source.type} is not supported yet`);
  }
  return executeLocalDaemonRefreshSource({ projectsRoot, projectId, source, signal });
}

export async function refreshLiveArtifact(options: RefreshLiveArtifactOptions): Promise<RefreshLiveArtifactResult> {
  return withLiveArtifactRefreshLock(options, async (lock) => {
    const refreshId = lock.metadata.refreshId;
    let sequence = 0;

    const appendLog = async (entry: {
      step: string;
      status: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'skipped';
      startedAt: Date;

View on GitHub (pinned to 5be4028344)

Solutions

  1. Make the connector tool return a top-level JSON object (wrap scalars/arrays, e.g. { items: [...] } or { value: 42 }).
  2. If the connector output cannot change, add an outputMapping.dataPaths on the source so the refresh reshapes the array/scalar into an object before merge.
  3. Verify the connector implementation's execute() resolves with { output: { ... } } and not { output: null }.

Example fix

// before: connector returns a bare array / scalar
const result = await connectorService.execute(req, ctx);
// result.output === ['a', 'b']  -> throws 'connector refresh output must be a JSON object'

// after: connector returns an object wrapping the payload
// result.output === { items: ['a', 'b'] }
Defensive patterns

Strategy: type-guard

Validate before calling

// Validate connector output shape before relying on it in a refresh flow.
function isPlainJsonObject(value: unknown): value is Record<string, unknown> {
  return value !== null && typeof value === 'object' && !Array.isArray(value);
}
const result = await connectorService.execute(req, ctx);
if (!isPlainJsonObject(result.output)) {
  throw new TypeError('expected connector output to be a JSON object');
}

Type guard

function isConnectorJsonObject(output: unknown): output is Record<string, unknown> {
  return output !== null && typeof output === 'object' && !Array.isArray(output);
}

Try / catch

try {
  await refreshLiveArtifact(opts);
} catch (err) {
  if (err instanceof Error && err.message === 'connector refresh output must be a JSON object') {
    // surface a user-facing 'connector returned an unexpected shape' message
  }
  throw err;
}

Prevention

When it happens

Trigger: A live artifact whose document.sourceJson.type === 'connector_tool' is refreshed, connectorService.execute succeeds, but result.output is null, a string/number/boolean, or a JSON array (Array.isArray === true).

Common situations: A connector tool returns a raw scalar (e.g. a single count or status string) instead of an object; a connector returns a top-level array of records; the connector service yields null on an empty result set; a custom connector implementation forgot to wrap its payload in an object.

Related errors


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