elsa-workflows/elsa-core · error · InvalidOperationException
The replacement link was removed after its target user was…
Error message
The replacement link was removed after its target user was deleted, but the previous link could not be restored.
What it means
Thrown inside CompensateReplacementAsync when it cannot prove the compensation committed: either no restoration attempt was made, the old link is missing, or the replacement link is still present. RemoveReplacementLinksOrThrowAsync first removes both links so no link references a deleted user, then this InvalidOperationException is thrown with the original compensation exception attached. The database ends up with neither link rather than a link to a deleted user.
Solutions
- Re-run the external sign-in: provisioning will recreate the correct link for the (still-live) old user mapping.
- Manually inspect ExternalIdentityLinks for both link ids and re-insert the old link if the user still exists.
- Investigate the inner compensationException (timeout, deadlock, constraint) and fix the underlying DB issue.
- Avoid concurrent replace operations on the same external identity (queue or lock per external subject).
Example fix
// manual repair when compensation left no link
INSERT INTO ExternalIdentityLinks (Id, UserId, Provider, Subject, ...) VALUES ('old-link-id', 'old-user-id', ...); Defensive patterns
Strategy: try-catch
Validate before calling
bool oldOk = await LinkExistsAsync(oldLink.Id, ct);
bool replGone = !await LinkExistsAsync(replacementLink.Id, ct);
if (!oldOk || !replGone) throw new InvalidOperationException("Compensation incomplete before retry"); Try / catch
try { await provisioner.ReplaceAsync(...); }
catch (InvalidOperationException ex) when (ex.Message.Contains("previous link could not be restored"))
{
logger.LogError(ex.InnerException, "Compensation failed; repair ExternalIdentityLinks manually");
// inspect both link ids and re-insert/repair before retrying
} Prevention
- Monitor the inner compensationException to catch DB-level root causes
- Avoid concurrent replaces on the same external identity
- Use transactions that guarantee the restore commits or rolls back fully
- Alert on this error — it means the DB is left with neither link
When it happens
Trigger: During replacement compensation the old link is not found (restoration transaction rolled back or was concurrently deleted), or the replacement link still exists after the restore attempt, i.e. restorationCommitted evaluates false.
Common situations: Database failure/timeout during the restore transaction; concurrent modification of the same link rows by another sign-in request; transaction scope mismatches leaving the restore uncommitted.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- The Elsa user was deleted while its external identity link…
- The restored previous link was removed after its user was…
- The Elsa user was deleted while its external identity link…
- A replacement-compensation link refers to a deleted user…
- Register with configured before calling , or call with a…
AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13).
Data as JSON: /api/errors/7fd9e36fdd7ca73c.
Report an issue: GitHub.
Appendix: source
Thrown at src/modules/Elsa.ExternalAuthentication.Persistence.EFCore/Stores/EFCoreExternalIdentityProvisioner.cs:317
UserId = oldLink.UserId,
CreatedAt = oldLink.CreatedAt,
LastSignedInAt = oldLink.LastSignedInAt
});
await dbContext.SaveChangesAsync(cancellationToken);
commitAttempted = true;
await transaction.CommitAsync(cancellationToken);
}
catch (Exception compensationException)
{
// The transaction scope has been disposed before this handler runs. A lost commit acknowledgement must
// not turn a successfully restored previous link into data loss.
var restorationCommitted = commitAttempted &&
await LinkExistsAsync(oldLink.Id, cancellationToken) &&
!await LinkExistsAsync(replacementLink.Id, cancellationToken);
if (!restorationCommitted)
{
await RemoveReplacementLinksOrThrowAsync(oldLink.Id, replacementLink.Id, compensationException, cancellationToken);
throw new InvalidOperationException(
"The replacement link was removed after its target user was deleted, but the previous link could not be restored.",
compensationException);
}
}
// An indeterminate user-directory failure must not be mistaken for a failed link restoration.
// Only remove the restored link when the directory positively reports that its user is gone.
var previousUser = new User { Id = oldLink.UserId, TenantId = oldLink.TenantId };
if (!await _userProvisioningService.ExistsAsync(previousUser, false, cancellationToken))
{
try
{
await using var cleanupContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
await cleanupContext.ExternalIdentityLinks.Where(x => x.Id == oldLink.Id).ExecuteDeleteAsync(cancellationToken);
}
catch (Exception cleanupException)
{
await RemoveReplacementLinksOrThrowAsync(oldLink.Id, replacementLink.Id, cleanupException, cancellationToken);View on GitHub (pinned to fe9217bdfa)