elsa-workflows/elsa-core · error · InvalidOperationException

Cannot overwrite an AI proposal that belongs to another…

Error message

Cannot overwrite an AI proposal that belongs to another user.

What it means

ValidateUserOwnership for proposals: when overwriting an existing proposal record that has a non-empty CreatedBy, the store requires the incoming proposal.CreatedBy to match exactly (ordinal). Mismatch throws InvalidOperationException to stop one creator overwriting another user's proposal.

Solutions

  1. Verify proposal.CreatedBy is populated with the same user who originally created the proposal.
  2. Use fresh IDs for each user's proposals instead of shared/deterministic IDs.
  3. Fetch the record first and only allow updates from the owning creator's context; otherwise create a new proposal.
  4. For admin flows, delete and recreate rather than overwriting another creator's record.

Example fix

// before
proposal.CreatedBy = null; // identity lost in service layer
await proposalStore.SaveAsync(proposal);
// after
proposal.CreatedBy = originalCreatedBy ?? currentUser.Id;
await proposalStore.SaveAsync(proposal);
Defensive patterns

Strategy: validation

Validate before calling

if (!string.IsNullOrWhiteSpace(proposal.Id) && string.IsNullOrWhiteSpace(proposal.CreatedBy)) throw new ArgumentException("CreatedBy is required and must match the record creator.");

Type guard

static bool HasCreator(AIProposal p) => !string.IsNullOrWhiteSpace(p.CreatedBy);

Try / catch

try { await proposalStore.SaveAsync(proposal); }
catch (InvalidOperationException ex) when (ex.Message.Contains("belongs to another user")) { /* reject update; require a fresh proposal for this creator */ }

Prevention

When it happens

Trigger: Calling SaveAsync (or the retry path) on an existing proposal where record.CreatedBy is non-whitespace and record.CreatedBy != proposal.CreatedBy.

Common situations: Reusing a proposal Id from another user's session; losing the authenticated user identity between create and update (CreatedBy empty or different); mapping bugs copying the wrong user field; importing proposals between users.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13). Data as JSON: /api/errors/f1e21183d84a1786. Report an issue: GitHub.

Appendix: source

Thrown at src/modules/Elsa.AI.Persistence.EFCore/Stores/EFCoreAIProposalStore.cs:126

        record.ReviewedBy = proposal.ReviewedBy;
        record.ReviewedAt = proposal.ReviewedAt;
        record.AppliedBy = proposal.AppliedBy;
        record.AppliedAt = proposal.AppliedAt;
    }

    private static TEnum ParseEnum<TEnum>(string value, TEnum defaultValue) where TEnum : struct =>
        Enum.TryParse<TEnum>(value, ignoreCase: true, out var result) ? result : defaultValue;

    private static bool BelongsToTenant(string? storedTenantId, string? requestedTenantId) =>
        string.Equals(NormalizeTenantId(storedTenantId), NormalizeTenantId(requestedTenantId), StringComparison.Ordinal);

    private static string NormalizeTenantId(string? tenantId) => tenantId ?? "";

    private static void ValidateUserOwnership(AIProposalRecord record, AIProposal proposal)
    {
        if (!string.IsNullOrWhiteSpace(record.CreatedBy) && !string.Equals(record.CreatedBy, proposal.CreatedBy, StringComparison.Ordinal))
            throw new InvalidOperationException("Cannot overwrite an AI proposal that belongs to another user.");
    }

    private static void Validate(AIProposal proposal)
    {
        if (string.IsNullOrWhiteSpace(proposal.Id))
            throw new ArgumentException("A proposal ID is required.", nameof(proposal));

        if (string.IsNullOrWhiteSpace(proposal.ConversationId))
            throw new ArgumentException("A proposal conversation ID is required.", nameof(proposal));

        if (string.IsNullOrWhiteSpace(proposal.CreatedBy))
            throw new ArgumentException("A proposal creator is required.", nameof(proposal));
    }
}

View on GitHub (pinned to fe9217bdfa)