{"record":{"id":"98dfe45b33820fa7","repo":"nexu-io/open-design","slug":"connector-refresh-output-must-be-a-json-object","errorCode":null,"errorMessage":"connector refresh output must be a JSON object","messagePattern":"connector refresh output must be a JSON object","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"apps/daemon/src/live-artifacts/refresh-service.ts","lineNumber":98,"sourceCode":"  projectId: string;\n  source: LiveArtifactSource;\n  signal: AbortSignal;\n}): Promise<BoundedJsonObject> {\n  const { projectsRoot, projectId, source, signal } = options;\n  if (source.type === 'connector_tool') {\n    const connector = source.connector;\n    if (connector === undefined) throw new Error('connector refresh source requires connector metadata');\n    const result = await connectorService.execute(\n      {\n        connectorId: connector.connectorId,\n        toolName: connector.toolName,\n        input: source.input,\n        ...(connector.accountLabel === undefined ? {} : { expectedAccountLabel: connector.accountLabel }),\n      },\n      { projectsRoot, projectId, purpose: 'artifact_refresh', signal },\n    );\n    if (result.output === null || typeof result.output !== 'object' || Array.isArray(result.output)) {\n      throw new Error('connector refresh output must be a JSON object');\n    }\n    return result.output;\n  }\n  if (source.type !== 'daemon_tool' && source.type !== 'local_file') {\n    throw new Error(`refresh source ${source.type} is not supported yet`);\n  }\n  return executeLocalDaemonRefreshSource({ projectsRoot, projectId, source, signal });\n}\n\nexport async function refreshLiveArtifact(options: RefreshLiveArtifactOptions): Promise<RefreshLiveArtifactResult> {\n  return withLiveArtifactRefreshLock(options, async (lock) => {\n    const refreshId = lock.metadata.refreshId;\n    let sequence = 0;\n\n    const appendLog = async (entry: {\n      step: string;\n      status: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'skipped';\n      startedAt: Date;","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/live-artifacts/refresh-service.ts#L80-L116","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Make the connector tool return a top-level JSON object (wrap scalars/arrays, e.g. { items: [...] } or { value: 42 }).","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.","Verify the connector implementation's execute() resolves with { output: { ... } } and not { output: null }."],"exampleFix":"// before: connector returns a bare array / scalar\nconst result = await connectorService.execute(req, ctx);\n// result.output === ['a', 'b']  -> throws 'connector refresh output must be a JSON object'\n\n// after: connector returns an object wrapping the payload\n// result.output === { items: ['a', 'b'] }","handlingStrategy":"type-guard","validationCode":"// Validate connector output shape before relying on it in a refresh flow.\nfunction isPlainJsonObject(value: unknown): value is Record<string, unknown> {\n  return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\nconst result = await connectorService.execute(req, ctx);\nif (!isPlainJsonObject(result.output)) {\n  throw new TypeError('expected connector output to be a JSON object');\n}","typeGuard":"function isConnectorJsonObject(output: unknown): output is Record<string, unknown> {\n  return output !== null && typeof output === 'object' && !Array.isArray(output);\n}","tryCatchPattern":"try {\n  await refreshLiveArtifact(opts);\n} catch (err) {\n  if (err instanceof Error && err.message === 'connector refresh output must be a JSON object') {\n    // surface a user-facing 'connector returned an unexpected shape' message\n  }\n  throw err;\n}","preventionTips":["Define the connector tool's output as an object schema and assert it in tests.","Wrap any scalar/array connector payload in an object before returning from execute().","Never let a connector resolve with output: null on empty results; use {} or { items: [] }."],"tags":["connector","live-artifacts","refresh","json-schema"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}