{"record":{"id":"1edc49c7836b8076","repo":"Budibase/budibase","slug":"relationship-error-invalid-value","errorCode":null,"errorMessage":"Relationship Error: Invalid value","messagePattern":"Relationship Error: Invalid value","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/server/src/db/linkedRows/LinkController.ts","lineNumber":184,"sourceCode":"   * When a row is saved this will carry out the necessary operations to make sure\n   * the link has been created/updated.\n   * @returns returns the row that has been cleaned and prepared to be written to the DB - links\n   * have also been created.\n   */\n  async rowSaved() {\n    const table = await this.table()\n    const row = this._row!\n    const operations = []\n    // get link docs to compare against\n    const linkDocs = (await this.getRowLinkDocs(row._id!)) as LinkDocument[]\n    for (let fieldName of Object.keys(table.schema)) {\n      // get the links this row wants to make\n      const rowField = row[fieldName]\n      const field = table.schema[fieldName]\n      if (field.type === FieldType.LINK && rowField != null) {\n        // Expects an array of docs with at least their _id\n        if (!Array.isArray(rowField)) {\n          throw new Error(\"Relationship Error: Invalid value\")\n        }\n\n        // check which links actual pertain to the update in this row\n        const thisFieldLinkDocs = linkDocs.filter(\n          linkDoc =>\n            linkDoc.doc1.fieldName === fieldName ||\n            linkDoc.doc2.fieldName === fieldName\n        )\n        const linkDocIds = thisFieldLinkDocs.map(linkDoc => {\n          return linkDoc.doc1.rowId === row._id\n            ? linkDoc.doc2.rowId\n            : linkDoc.doc1.rowId\n        })\n\n        // if 1:N, ensure that this ID is not already attached to another record\n        const linkedTable = await this._db.get<Table>(field.tableId)\n        const linkedSchema = linkedTable.schema[field.fieldName]\n","sourceCodeStart":166,"sourceCodeEnd":202,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/db/linkedRows/LinkController.ts#L166-L202","documentation":"`rowSaved` processes link fields after a row save. For each link-type field, the row's value must be an array of link docs (each containing at least an _id). If the field value is non-null but not an array — e.g. a single id string, an object, or a number — the controller cannot compute link diffs and throws 'Relationship Error: Invalid value'.","triggerScenarios":"Saving/updating a row where a link field is set to \"row_id_123\" (single string) instead of [\"row_id_123\"], or to an object like {\"_id\":\"x\"}, via the REST /rows endpoint, an automation Create/Update Row step, or the builder UI binding a scalar into a multi-select link field.","commonSituations":"API clients forgetting link fields are arrays; automations passing a query-result string straight into a link field; form components bound to the wrong type producing a single value instead of an array of selected row ids.","solutions":["Wrap the value in an array of ids: field: [\"id1\", \"id2\"] instead of field: \"id1\".","In automations, add a step (JS/script) that normalizes the binding to an array before the row update.","In the builder, bind the link field to the multi-select's array of values, not its raw string."],"exampleFix":"// before\nawait api.post(`/tables/${tableId}/rows`, { ...row, related: \"row_456\" })\n// after\nawait api.post(`/tables/${tableId}/rows`, { ...row, related: [\"row_456\"] })","handlingStrategy":"type-guard","validationCode":"const isLinkIds = (v: unknown): boolean =>\n  v == null || (Array.isArray(v) && v.every(x => typeof x === \"string\"))\nif (!isLinkIds(row.related)) throw new Error(\"link fields must be arrays of row ids (or null)\")","typeGuard":"const isLinkIdArray = (v: unknown): v is string[] =>\n  Array.isArray(v) && v.every((x): x is string => typeof x === \"string\")","tryCatchPattern":"try {\n  await api.post(`/tables/${tableId}/rows`, row)\n} catch (err) {\n  if (err.message.includes(\"Relationship Error: Invalid value\")) {\n    for (const [k, v] of Object.entries(row)) {\n      if (linkFields.has(k) && v != null && !Array.isArray(v)) row[k] = [v]\n    }\n    return api.post(`/tables/${tableId}/rows`, row)\n  }\n  throw err\n}","preventionTips":["Always send link fields as arrays of row ids.","Normalize upstream bindings (query results, form values) to arrays before row updates.","In automations, insert a script step to coerce scalars into arrays for link fields.","Bind multi-select components' value arrays, not their string representations."],"tags":["database","linked-rows","relationships","validation","rows"],"backgroundTag":"link-field-must-be-array","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}