elsa-workflows/elsa-core · error · KeyNotFoundException

User task ' ' was not found.

Error message

User task '{task.Id}' was not found.

What it means

VNextUserTaskRepository.SaveAsync performs a load-modify-save against the document store. If LoadDocumentAsync finds no stored document for the task's tenant and id, SaveAsync throws KeyNotFoundException because updates are only valid for existing tasks. Missing documents are treated as update-of-nonexistent-entity rather than an implicit insert.

Solutions

  1. Create the user task via the repository's create/insert API before calling SaveAsync.
  2. Verify task.Id and task.TenantId correspond to an existing document.
  3. Catch KeyNotFoundException and surface a not-found error, or implement upsert logic if insert-on-save is intended.

Example fix

// before
var task = new UserTask { Id = taskId, TenantId = tenantId, Title = "Review" };
await repo.SaveAsync(task, expectedRevision: 0, ct); // throws if not created first
// after
var created = await repo.CreateAsync(task, ct); // insert path first
await repo.SaveAsync(created, created.Revision, ct);
Defensive patterns

Strategy: try-catch

Validate before calling

var exists = await repo.GetAsync(task.TenantId, task.Id, ct);
if (exists is null) throw new InvalidOperationException("Create the task before saving updates.");

Try / catch

try
{
    await repo.SaveAsync(task, expectedRevision, ct);
}
catch (KeyNotFoundException)
{
    logger.LogWarning("User task {TaskId} not found in tenant {TenantId}; skipping save.", task.Id, task.TenantId);
}

Prevention

When it happens

Trigger: Calling SaveAsync(task, expectedRevision) for a UserTask whose document (tenantId:taskId) was never created via a create/insert path, or was deleted beforehand.

Common situations: Replaying events or migrating data where the create step was skipped; task id mismatch (e.g., using a correlation id instead of the task id); cross-tenant saves where the document lives under a different tenant.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at src/modules/Elsa.UserTasks.Persistence.VNext/Repositories/VNextUserTaskRepository.cs:72

    public async Task<(UserTask Task, UserTaskInvitation Invitation)?> FindByInvitationTokenHashAsync(string tokenHash, CancellationToken cancellationToken = default)
    {
        // The document store has no cross-tenant secondary index for invitation hashes, so this scans the
        // storage unit. Verification is rate limited and rare; correctness matters more than the scan here.
        foreach (var task in await LoadAllAsync(cancellationToken))
        {
            var invitation = task.Invitations.FirstOrDefault(x => string.Equals(x.TokenHash, tokenHash, StringComparison.Ordinal));
            if (invitation != null)
                return (task, invitation);
        }
        return null;
    }

    public async Task SaveAsync(UserTask task, int expectedRevision, CancellationToken cancellationToken = default)
    {
        var existing = await LoadDocumentAsync(task.TenantId, task.Id, cancellationToken);
        if (existing is null)
            throw new KeyNotFoundException($"User task '{task.Id}' was not found.");
        var loaded = existing.Value;
        if (loaded.Task.Revision != expectedRevision)
            throw new UserTaskRevisionConflictException(task.Id, expectedRevision);

        task.Revision = expectedRevision + 1;
        task.UpdatedAt = DateTimeOffset.UtcNow;
        try
        {
            await documentStore.SaveAsync(CreateRequest(task, loaded.Document.Version), cancellationToken);
        }
        catch (DocumentStoreConcurrencyException exception)
        {
            // A writer won the race between the read above and this save. Same contract as the other
            // providers so the caller sees one exception type regardless of the installed store.
            throw new UserTaskRevisionConflictException(task.Id, expectedRevision, exception);
        }
    }

View on GitHub (pinned to fe9217bdfa)