elsa-core/doc/wiki/output-converters.md
2026-07-31 04:10:48 +02:00

4.5 KiB

Output Converters

Output converters let an extension transform an activity's native output for one bound variable or workflow output. Conversion is explicit and opt-in: activity-output registers and diagnostics keep the native value, while only the binding destination receives the converted value.

Implement And Register A Converter

Converters are synchronous, deterministic, and side-effect free. Their context contains only the native value, declared source and destination types, and immutable JSON settings.

using System.Globalization;
using System.Text.Json;
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Models;

public sealed class NumberToTextConverter : IOutputConverter
{
    public object Convert(OutputConversionContext context)
    {
        var format = context.Settings?.TryGetProperty("format", out var value) == true
            ? value.GetString()
            : null;

        return ((decimal)context.Value).ToString(format, CultureInfo.InvariantCulture);
    }
}

using var schemaDocument = JsonDocument.Parse(
    """{"type":"object","properties":{"format":{"type":"string"}}}""");

services.AddOutputConverter<NumberToTextConverter>(
    new OutputConverterDescriptor(
        "sample.number-to-text.v1",
        typeof(decimal),
        typeof(string),
        "Number to text",
        "Formats a decimal using an explicit invariant format.",
        schemaDocument.RootElement));

The converter ID is a persisted public contract. Keep lookup case-sensitive, do not reuse an ID for breaking behavior or settings changes, and register a new versioned ID instead.

The default service lifetime is scoped. Elsa resolves the implementation from the active workflow execution scope; the registry caches descriptors and registrations, not converter instances.

Configure A Binding

Converter configuration belongs to an Output<T> binding:

activity.Result = new Output<decimal>(targetVariable)
{
    Converter = new OutputConverterConfiguration(
        "sample.number-to-text.v1",
        JsonDocument.Parse("""{"format":"0.00"}""").RootElement)
};

The serialized binding adds one optional object:

{
  "typeName": "Decimal",
  "memoryReference": {
    "id": "formatted-total"
  },
  "converter": {
    "id": "sample.number-to-text.v1",
    "settings": {
      "format": "0.00"
    }
  }
}

Bindings without a converter omit converter and retain the existing assignment path.

Validation And Failure Behavior

At definition validation and again at runtime, Elsa checks:

  • the binding has a resolvable variable or workflow-output destination;
  • the converter ID is registered;
  • the declared output type is compatible with the converter source type;
  • the converter result type is assignable to the destination;
  • JSON Schema and converter-owned settings validation pass.

Null native values bypass converter invocation and are delivered only to destinations that permit null. Conversion and result validation finish before Elsa writes the destination, so a failure leaves it unchanged.

Runtime failures fault through Elsa's normal activity fault pipeline as OutputConversionException. Persisted exception state includes safe structured identities and the failure stage; it excludes native values, raw settings, and converter exception details.

Discovery API And Studio

Studio queries:

GET /descriptors/output-converters?sourceType=Decimal&destinationType=String

The endpoint requires read:* or read:output-converters. It returns compatible IDs, declared type names, display metadata, and optional settings schemas. It never exposes converter instances, implementation types, or service lifetimes.

Studio offers schema-driven fields for simple object schemas and a raw JSON-object editor otherwise. Unknown persisted converter IDs remain visible, and older servers that do not expose discovery do not cause Studio to delete existing configuration.

Operational Guidance

  • Make locale, timezone, rounding, and similar environmental choices explicit settings.
  • Do not perform I/O or mutate workflow state from a converter.
  • Treat converter removal as deployment drift: previously published workflows that reference it will fault at assignment.
  • Use activity inputs or an explicit activity when transformation is asynchronous or has side effects.

The design decisions are recorded in ADR 0011, ADR 0012, and ADR 0013.