Thanks for your answer!
It makes sense. I had already split the query to create the user separately, however both queries were part of a bigger transaction created by java @Transactional annotation. I removed it to test and started working correctly.
Your suggested approach solves the issue and allows creating the user, if needed, in the same query.
I had to do some adjustments, but the essence is still the same:
WITH "123" as myUserId
CALL apoc.periodic.iterate(
"RETURN 1",
"MERGE (user:User {userId: myUserId})", {batchSize:1, iterateList:true, parallel:true, params: {userId:userId, appId:$appId}}) yield committedOperations
MATCH (user:User {userId: myUserId})
WITH user
CALL apoc.periodic.iterate(
"WITH $user as user UNWIND $friendsToAdd AS userId return userId, user",
"MERGE (friend:User {userId: userId}) MERGE (user)-[:IS_FRIEND_OF]-(friend)", {batchSize:100, iterateList:true, parallel:true, params: {user:user, friendsToAdd:$friendsToAdd}}
) YIELD committedOperations, errorMessages
RETURN committedOperations, errorMessages
I had to use apoc.periodic.iterate to create the user because neither apoc.cypher.run nor apoc.cypher.runMany can't be used for write statements.
apoc.cypher.doIt does actually allow write statements, but doesn't run them in a separate transaction afaik.
Thanks again for the help.