{"record":{"id":"7fcca27cdc4b34ae","repo":"nocobase/nocobase","slug":"flowmodels-attach-subkey-subkey-already-exist","errorCode":null,"errorMessage":"flowModels:attach subKey '${subKey}' already exists on parent '${parentId}'","messagePattern":"flowModels:attach subKey '(.+?)' already exists on parent '(.+?)'","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/plugins/@nocobase/plugin-flow-engine/src/server/repository.ts","lineNumber":1930,"sourceCode":"                            ON NodeInfo.descendant = TreeTable.descendant\n                                AND NodeInfo.depth = 0\n         WHERE TreeTable.depth = 1\n           AND TreeTable.ancestor = :ancestor\n           AND NodeInfo.type = :type\n           AND TreeTable.descendant != :uid\n         LIMIT 1`,\n        {\n          type: 'SELECT',\n          replacements: {\n            ancestor: parentId,\n            type: subKey,\n            uid: modelUid,\n          },\n          transaction,\n        },\n      );\n      if (conflict?.length) {\n        throw new Error(`flowModels:attach subKey '${subKey}' already exists on parent '${parentId}'`);\n      }\n    }\n\n    const normalizePosition = (input: unknown): FlowModelAttachPosition => {\n      if (!input) return 'last';\n      if (input === 'first' || input === 'last') return input;\n      if (typeof input === 'object') {\n        const p = input as any;\n        const type = p?.type;\n        const target = String(p?.target || '').trim();\n        if ((type === 'before' || type === 'after') && target) {\n          return { type, target };\n        }\n      }\n      throw new Error('flowModels:attach invalid position');\n    };\n\n    const position: FlowModelAttachPosition =","sourceCodeStart":1912,"sourceCodeEnd":1948,"githubUrl":"https://github.com/nocobase/nocobase/blob/fa42722fefe44265490dff2c27d79e2882bce4fa/packages/plugins/@nocobase/plugin-flow-engine/src/server/repository.ts#L1912-L1948","documentation":"Thrown by FlowModelRepository.attach when the model being attached has subType 'object' and another child model already occupies the same subKey under the target parent. Object sub-models are keyed maps, so each subKey must be unique per parent; attaching a second model with the same key would silently overwrite the existing child, so the repository rejects it.","triggerScenarios":"Calling flowModels.attach (or the flowModels:attach server action) with subType='object' where a sibling row already exists at depth=1 under parentId with NodeInfo.type === subKey, and that sibling's uid differs from the model being attached.","commonSituations":"Re-attaching a moved model after a failed/partial cleanup left the old row in the tree table; two UI panels concurrently creating a child under the same well-known key like 'detail' or 'form'; copying a model definition without renaming its subKey.","solutions":["Query the existing children of parentId first and pick an unused subKey, or delete/detach the conflicting child before re-attaching.","If the model is being moved back to a former parent, ensure the previous attach's transaction completed/rolled back so stale tree rows were removed.","Catch the error and surface it to the user as 'a child with this key already exists' rather than retrying blindly."],"exampleFix":"// before\nawait repo.attach({ uid, parentId, subKey: 'items', subType: 'object' });\n// after\nconst children = await repo.findChildren(parentId);\nif (children.some((c) => c.subKey === 'items')) subKey = 'items-2';\nawait repo.attach({ uid, parentId, subKey, subType: 'object' });","handlingStrategy":"validation","validationCode":"const children = await repo.findChildren(parentId);\nif (subType === 'object' && children.some((c) => c.subKey === subKey && c.uid !== uid)) {\n  throw new SkipAttachError(`subKey '${subKey}' taken`);\n}\nawait repo.attach({ uid, parentId, subKey, subType });","typeGuard":"const isFreeSubKey = (c: { subKey: string; uid: string }[], subKey: string, uid: string) =>\n  !c.some((x) => x.subKey === subKey && x.uid !== uid);","tryCatchPattern":"try {\n  await repo.attach({ uid, parentId, subKey, subType: 'object' });\n} catch (e) {\n  if (String(e.message).startsWith('flowModels:attach subKey')) {\n    notifyUser('A child with this key already exists here.');\n  } else throw e;\n}","preventionTips":["List existing children of the parent before choosing an object subKey","Generate unique subKeys (suffix -2, -3) instead of reusing well-known keys","Clean up tree rows when detaching so old entries cannot conflict later"],"tags":["flow-engine","tree","duplicate-key","validation"],"backgroundTag":"duplicate-child-key","analyzedSha":"fa42722fefe44265490dff2c27d79e2882bce4fa","analyzedAt":"2026-09-01T00:54:31.202Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}