elsa-workflows/elsa-core · error · InvalidOperationException

Platform runtime command artifact does not include a…

Error message

Platform runtime command artifact does not include a download URL.

What it means

DownloadArtifactAsync downloads the payload of a Platform runtime command artifact. The Platform must supply a DownloadUrl on each artifact; when it is missing or blank, the client refuses to build a request and throws this InvalidOperationException.

Solutions

  1. Inspect the raw command response to confirm whether the Platform actually omitted DownloadUrl; upgrade/align the Platform service if it is an API version mismatch.
  2. Re-trigger the command on the Platform so the artifact (and its URL) is regenerated.
  3. Add a guard before calling DownloadArtifactAsync: skip or surface an actionable error when DownloadUrl is null.

Example fix

// before
var bytes = await client.DownloadArtifactAsync(command, artifact, leaseToken);
// after
if (string.IsNullOrWhiteSpace(artifact.DownloadUrl))
    throw new InvalidOperationException($"Artifact {artifact.Name} has no download URL; re-issue the command.");
var bytes = await client.DownloadArtifactAsync(command, artifact, leaseToken);
Defensive patterns

Strategy: type-guard

Validate before calling

if (string.IsNullOrWhiteSpace(artifact.DownloadUrl))
    throw new InvalidOperationException($"Artifact '{artifact.Name}' lacks a download URL; re-issue the command.");

Type guard

bool IsDownloadable(PlatformArtifactItem a) => !string.IsNullOrWhiteSpace(a.DownloadUrl);

Try / catch

try { var bytes = await client.DownloadArtifactAsync(command, artifact, leaseToken, ct); }
catch (InvalidOperationException ex) when (ex.Message.Contains("download URL"))
{
    logger.LogWarning(ex, "Artifact for command {CommandId} has no download URL.", command.Id);
}

Prevention

When it happens

Trigger: Polling returns a command whose artifact has DownloadUrl null/empty/whitespace, then DownloadArtifactAsync is called for that artifact.

Common situations: Platform API version mismatch returning an older artifact shape without download URLs; the artifact upload on the Platform side failed or expired, so the URL was omitted; deserialization silently left the property null due to JSON casing settings.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


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

Appendix: source

Thrown at src/modules/Elsa.Platform.Integration/Services/PlatformRuntimeCommandClient.cs:52

            BuildUri($"/deployments/runtime/commands/{commandId:D}/claim"),
            new PlatformRuntimeCommandClaimRequest(_options.EngineId, _options.WorkerId, (int)_options.ClaimLeaseDuration.TotalSeconds),
            cancellationToken);

        if (response.StatusCode == HttpStatusCode.Conflict || response.StatusCode == HttpStatusCode.NotFound)
            return null;

        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<PlatformRuntimeCommandClaimResponse>(JsonOptions, cancellationToken);
    }

    public async Task<Stream> DownloadArtifactAsync(
        PlatformRuntimeCommand command,
        PlatformArtifactItem artifact,
        string leaseToken,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(artifact.DownloadUrl))
            throw new InvalidOperationException("Platform runtime command artifact does not include a download URL.");

        using var request = CreateRequest(HttpMethod.Get, BuildUri(artifact.DownloadUrl));
        request.Headers.Add(LeaseHeaderName, leaseToken);
        request.Headers.Add(WorkerHeaderName, _options.WorkerId);
        using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
        response.EnsureSuccessStatusCode();
        var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
        var buffer = new MemoryStream();
        await CopyBoundedAsync(stream, buffer, cancellationToken);
        buffer.Position = 0;
        return buffer;
    }

    public Task ReportProgressAsync(
        Guid commandId,
        string leaseToken,
        string status,
        int? percentComplete,

View on GitHub (pinned to fe9217bdfa)