Budibase/budibase · error · HTTPError

Cannot insert rows through a calculation view

Error message

Cannot insert rows through a calculation view

What it means

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.

Source

Thrown at packages/server/src/sdk/workspace/rows/external.ts:59

  const rows = response?.rows || []
  return rows[0]
}

export async function save(
  sourceId: string,
  inputs: Row,
  userId: string | undefined
) {
  const { tableId, viewId } = tryExtractingTableAndViewId(sourceId)
  let source: Table | ViewV2
  if (viewId) {
    source = await sdk.views.get(viewId)
  } else {
    source = await sdk.tables.getTable(tableId)
  }

  if (sdk.views.isView(source) && helpers.views.isCalculationView(source)) {
    throw new HTTPError("Cannot insert rows through a calculation view", 400)
  }

  const row = await inputProcessing(userId, cloneDeep(source), inputs)

  const validateResult = await sdk.rows.utils.validate({
    row,
    source,
  })
  if (!validateResult.valid) {
    throw { validation: validateResult.errors }
  }

  const response = await handleRequest(Operation.CREATE, source, {
    row,
  })

  const rowId = response.row._id
  if (rowId) {

View on GitHub (pinned to a81a902e9a)

Solutions

  1. Insert rows against the underlying table instead: pass tableId (source.tableId from the view) rather than the calculation view's viewId.
  2. Detect calculation views before saving (helpers.views.isCalculationView) and route writes to the base table.
  3. Update any UI/form binding that targets the calculation view to bind the create action to the source table.

Example fix

// before
await rows.save({ viewId: calcViewId, row }) // throws
// after
const view = await sdk.views.get(calcViewId)
if (helpers.views.isCalculationView(view)) {
  await rows.save({ tableId: view.tableId, row })
}
Defensive patterns

Strategy: type-guard

Validate before calling

const view = await sdk.views.get(viewId)
if (helpers.views.isCalculationView(view)) {
  throw new Error("Cannot save rows through a calculation view — write to the source table instead")
}

Type guard

const isWritableSource = (source: Table | View): source is Table =>
  !sdk.views.isView(source) || !helpers.views.isCalculationView(source)

Try / catch

try {
  await rows.save({ viewId, row })
} catch (e) {
  if (e instanceof HTTPError && e.status === 400 && e.message.includes("calculation view")) {
    // redirect write to view.tableId
  } else throw e
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of Budibase/budibase@a81a902e9a (2026-08-29). Data as JSON: /api/errors/b36dd43ccd726d64. Report an issue: GitHub.