{"record":{"id":"da5d8a6d169d57ad","repo":"flowable/flowable-engine","slug":"only-one-of-user-or-group-can-be-used-to-create-an","errorCode":null,"errorMessage":"Only one of user or group can be used to create an identity link.","messagePattern":"Only one of user or group can be used to create an identity link\\.","errorType":"http","errorClass":"FlowableIllegalArgumentException","httpStatus":400,"severity":"warning","filePath":"modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/repository/CaseDefinitionIdentityLinkCollectionResource.java","lineNumber":78,"sourceCode":"    @ApiOperation(value = \"Add a candidate starter to a case definition\", tags = { \"Case Definitions\" },\n            notes = \"It is possible to add either a user or a group.\", code = 201)\n    @ApiResponses(value = {\n            @ApiResponse(code = 201, message = \"Indicates the case definition was found and the identity link was created.\"),\n            @ApiResponse(code = 400, message = \"Indicates the body does not contain the correct information.\"),\n            @ApiResponse(code = 404, message = \"Indicates the requested case definition was not found.\")\n    })\n    @PostMapping(value = \"/cmmn-repository/case-definitions/{caseDefinitionId}/identitylinks\", produces = \"application/json\")\n    @ResponseStatus(HttpStatus.CREATED)\n    public RestIdentityLink createIdentityLink(@ApiParam(name = \"caseDefinitionId\") @PathVariable String caseDefinitionId, @RequestBody RestIdentityLink identityLink) {\n\n        CaseDefinition caseDefinition = getCaseDefinitionFromRequestWithoutAccessCheck(caseDefinitionId);\n\n        if (identityLink.getGroup() == null && identityLink.getUser() == null) {\n            throw new FlowableIllegalArgumentException(\"A group or a user is required to create an identity link.\");\n        }\n\n        if (identityLink.getGroup() != null && identityLink.getUser() != null) {\n            throw new FlowableIllegalArgumentException(\"Only one of user or group can be used to create an identity link.\");\n        }\n\n        if (restApiInterceptor != null) {\n            restApiInterceptor.createCaseDefinitionIdentityLink(caseDefinition, identityLink);\n        }\n\n        if (identityLink.getGroup() != null) {\n            repositoryService.addCandidateStarterGroup(caseDefinition.getId(), identityLink.getGroup());\n        } else {\n            repositoryService.addCandidateStarterUser(caseDefinition.getId(), identityLink.getUser());\n        }\n\n        // Always candidate for case definition. User-provided value is ignored\n        identityLink.setType(IdentityLinkType.CANDIDATE);\n\n        return restResponseFactory.createRestIdentityLink(identityLink.getType(), identityLink.getUser(), identityLink.getGroup(), null, caseDefinition.getId(), null);\n    }\n","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/flowable/flowable-engine/blob/d6d39ce1c69ff244f2d9dc6af756a9b95e865586/modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/repository/CaseDefinitionIdentityLinkCollectionResource.java#L60-L96","documentation":"Immediately after the null-check, createIdentityLink rejects bodies where BOTH 'user' and 'group' are set: a Flowable identity link is either to a user or to a group, never both. Setting both is ambiguous, so FlowableIllegalArgumentException is thrown (HTTP 400).","triggerScenarios":"POST /cmmn-repository/case-definitions/{caseDefinitionId}/identity-links with a body containing both fields, e.g. {\"type\":\"candidate\",\"user\":\"john\",\"group\":\"sales\"}.","commonSituations":"Clients that echo back a whole identity-link object populated with defaults; forms that always submit both inputs; generic sync tools mapping an ACL entry with both subject types onto one link.","solutions":["Send exactly one of 'user' or 'group'; issue two separate POSTs if both are needed.","Strip the unused field in the client before serializing the payload.","If the intent is 'user who is member of group', just send the user and let group membership resolve it."],"exampleFix":"// before\n{\"type\":\"candidate\",\"user\":\"john\",\"group\":\"sales\"}\n// after\n{\"type\":\"candidate\",\"user\":\"john\"}\n// second call if needed:\n{\"type\":\"candidate\",\"group\":\"sales\"}","handlingStrategy":"validation","validationCode":"function validateIdentityLink(link) {\n  if (link.user && link.group) throw new Error('Send user OR group, not both');\n  if (!link.user && !link.group) throw new Error('Send user or group');\n  return true;\n}","typeGuard":"const isExclusiveIdentityLink = (l) =>\n  Boolean(l) && (Boolean(l.user) !== Boolean(l.group));","tryCatchPattern":"try {\n  await post(identityLinksUrl, link);\n} catch (e) {\n  if (e.response?.status === 400 && /Only one of user or group/.test(e.response.data?.message)) {\n    // split into two posts, one per subject\n  }\n  throw e;\n}","preventionTips":["Normalize payloads to a single subject field before sending.","Issue one request per identity link instead of batching user+group.","Add a unit test asserting user XOR group in serialized payloads."],"tags":["rest-api","validation","identity-link"],"backgroundTag":"mutually-exclusive-options","analyzedSha":"d6d39ce1c69ff244f2d9dc6af756a9b95e865586","analyzedAt":"2026-09-11T06:41:19.413Z","contentChangedAt":"2026-09-11T06:41:19.413Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}