{"record":{"id":"b36dd43ccd726d64","repo":"Budibase/budibase","slug":"cannot-insert-rows-through-a-calculation-view","errorCode":null,"errorMessage":"Cannot insert rows through a calculation view","messagePattern":"Cannot insert rows through a calculation view","errorType":"http","errorClass":"HTTPError","httpStatus":400,"severity":"error","filePath":"packages/server/src/sdk/workspace/rows/external.ts","lineNumber":59,"sourceCode":"  const rows = response?.rows || []\n  return rows[0]\n}\n\nexport async function save(\n  sourceId: string,\n  inputs: Row,\n  userId: string | undefined\n) {\n  const { tableId, viewId } = tryExtractingTableAndViewId(sourceId)\n  let source: Table | ViewV2\n  if (viewId) {\n    source = await sdk.views.get(viewId)\n  } else {\n    source = await sdk.tables.getTable(tableId)\n  }\n\n  if (sdk.views.isView(source) && helpers.views.isCalculationView(source)) {\n    throw new HTTPError(\"Cannot insert rows through a calculation view\", 400)\n  }\n\n  const row = await inputProcessing(userId, cloneDeep(source), inputs)\n\n  const validateResult = await sdk.rows.utils.validate({\n    row,\n    source,\n  })\n  if (!validateResult.valid) {\n    throw { validation: validateResult.errors }\n  }\n\n  const response = await handleRequest(Operation.CREATE, source, {\n    row,\n  })\n\n  const rowId = response.row._id\n  if (rowId) {","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/sdk/workspace/rows/external.ts#L41-L77","documentation":"Thrown by save in packages/server/src/sdk/workspace/rows/external.ts when attempting to insert rows through a calculation view. Calculation views are derived/aggregated representations and do not accept new rows, so save rejects the operation up front before input processing and validation.","triggerScenarios":"Calling save({ ..., viewId }) where the resolved source is a view for which sdk.views.isView(source) && helpers.views.isCalculationView(source) is true — i.e. the viewId points to a calculation view and rows are being inserted rather than read.","commonSituations":"Frontend or script reusing the same view id for both reads and writes; UI incorrectly binding a create-form to a calculation view; code refactors that swapped a table id for a view id in save calls.","solutions":["Insert rows against the underlying table instead: pass tableId (source.tableId from the view) rather than the calculation view's viewId.","Detect calculation views before saving (helpers.views.isCalculationView) and route writes to the base table.","Update any UI/form binding that targets the calculation view to bind the create action to the source table."],"exampleFix":"// before\nawait rows.save({ viewId: calcViewId, row }) // throws\n// after\nconst view = await sdk.views.get(calcViewId)\nif (helpers.views.isCalculationView(view)) {\n  await rows.save({ tableId: view.tableId, row })\n}","handlingStrategy":"type-guard","validationCode":"const view = await sdk.views.get(viewId)\nif (helpers.views.isCalculationView(view)) {\n  throw new Error(\"Cannot save rows through a calculation view — write to the source table instead\")\n}","typeGuard":"const isWritableSource = (source: Table | View): source is Table =>\n  !sdk.views.isView(source) || !helpers.views.isCalculationView(source)","tryCatchPattern":"try {\n  await rows.save({ viewId, row })\n} catch (e) {\n  if (e instanceof HTTPError && e.status === 400 && e.message.includes(\"calculation view\")) {\n    // redirect write to view.tableId\n  } else throw e\n}","preventionTips":["Bind create/insert forms to tables, never to calculation views.","Check helpers.views.isCalculationView before any write through a view.","For aggregated data, write to the base table and read back through the calculation view."],"tags":["validation","views","rows","write-operation","http-400"],"backgroundTag":"read-only-target-write","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}