{"record":{"id":"ca1584b9d74d5c66","repo":"medusajs/medusa","slug":"cannot-create-role-parent-relationship-this-would","errorCode":null,"errorMessage":"Cannot create role parent relationship: this would create a circular dependency (role_id: ${role_id}, parent_id: ${parent_id})","messagePattern":"Cannot create role parent relationship: this would create a circular dependency \\(role_id: (.+?), parent_id: (.+?)\\)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/modules/rbac/src/services/rbac-module-service.ts","lineNumber":262,"sourceCode":"    @MedusaContext() sharedContext: Context = {}\n  ): Promise<RbacRoleParentDTO[]> {\n    for (const parent of data) {\n      const { role_id, parent_id } = parent\n\n      if (role_id === parent_id) {\n        throw new Error(\n          `Cannot create role parent relationship: a role cannot be its own parent (role_id: ${role_id})`\n        )\n      }\n\n      const wouldCreateCycle = await this.rbacRepository_.checkForCycle(\n        role_id,\n        parent_id,\n        sharedContext\n      )\n\n      if (wouldCreateCycle) {\n        throw new Error(\n          `Cannot create role parent relationship: this would create a circular dependency (role_id: ${role_id}, parent_id: ${parent_id})`\n        )\n      }\n    }\n\n    return await super.createRbacRoleParents(data, sharedContext)\n  }\n\n  @InjectManager()\n  // @ts-expect-error\n  async updateRbacRoleParents(\n    data: UpdateRbacRoleParentDTO[],\n    @MedusaContext() sharedContext: Context = {}\n  ): Promise<RbacRoleParentDTO[]> {\n    for (const parent of data) {\n      const { role_id, parent_id } = parent\n\n      if (parent_id) {","sourceCodeStart":244,"sourceCodeEnd":280,"githubUrl":"https://github.com/medusajs/medusa/blob/5e06e544a296b9033f20f71f11c559f81a0e5739/packages/modules/rbac/src/services/rbac-module-service.ts#L244-L280","documentation":"Thrown by the RBAC module when creating a role-parent relationship that would introduce a cycle in the role hierarchy (e.g. A parent of B and B parent of A). checkForCycle runs against the repository before insert, so the transaction never persists a cyclic graph.","triggerScenarios":"Calling createRbacRoleParents where the proposed parent already has the current role as an ancestor — for example setting role A's parent to B after B's parent was set to A — including via setRoleParentStep or the createRbacRoleParents workflow.","commonSituations":"Bulk-importing role hierarchies from flat data where ordering creates temporary cycles, concurrent updates that each look acyclic individually, or admin UI allowing arbitrary parent selection without cycle checks.","solutions":["Reorder hierarchy imports so parents are always created before children (topological order)","Pick a different parent that is not a descendant of the current role","Break the existing cycle first by removing or re-pointing the offending relation, then retry the create"],"exampleFix":"// before\n// B already has parent A\nawait rbacService.createRbacRoleParents([\n  { role_id: a, parent_id: b }, // creates A -> B -> A cycle\n])\n// after\nawait rbacService.createRbacRoleParents([\n  { role_id: c, parent_id: b }, // leaf role instead\n])","handlingStrategy":"validation","validationCode":"async function wouldCycle(rbac: any, roleId: string, parentId: string) {\n  // walk up from parentId; if we reach roleId, it's a cycle\n  const seen = new Set<string>()\n  let current = parentId\n  while (current && !seen.has(current)) {\n    if (current === roleId) return true\n    seen.add(current)\n    const parents = await rbac.listRbacRoleParents({ role_id: current })\n    current = parents[0]?.parent_id\n  }\n  return false\n}","typeGuard":"async function isAcyclicParent(rbac: any, rel: { role_id: string; parent_id: string }): Promise<boolean> {\n  return rel.role_id !== rel.parent_id && !(await wouldCycle(rbac, rel.role_id, rel.parent_id))\n}","tryCatchPattern":"try {\n  await rbacService.createRbacRoleParents(payload)\n} catch (e) {\n  if (/circular dependency/.test(e.message)) {\n    // skip the offending relation and continue with the rest\n    continueImport()\n  } else throw e\n}","preventionTips":["Import hierarchies in topological order (parents first)","Maintain an in-memory role tree during bulk operations and check descendants before linking","Serialize hierarchy mutations behind a queue to avoid concurrent cycle misses"],"tags":["rbac","roles","cycle-detection","hierarchy"],"backgroundTag":"circular-dependency-detected","analyzedSha":"5e06e544a296b9033f20f71f11c559f81a0e5739","analyzedAt":"2026-08-27T07:24:39.599Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}