Skip to content
iQBus HomeDocumentationBlogContact
Updated August 11, 2026
Build & extend/Pipeline components
Developer Reference

Pipeline components

Build reusable message processing logic that Art2link ESB compiles and deploys automatically, no build steps, no restarts, no downtime.

A Pipeline Component is a C# class that processes messages flowing through an Art2link ESB Pipeline. Each component receives a message, applies your logic, and returns one or more messages to the next stage of the pipeline.

Art2link ESB handles compilation and deployment automatically. You write the code, save it, and reference it from a Pipeline, no build steps, no restarts, no downtime.

The architecture follows a simple chain: Port → Pipeline → Component(s) → Output. A Pipeline can contain multiple components executed in sequence. Each component operates on the output of the previous one.

Two kinds of work happen inside a component. The obvious one is transformation, reshape the body, enrich it, compress, sign, validate. The other is classification: every message the component emits carries a Message Type, assigned by setting MessageType on the outgoing PipelineMessage. Classification is not a second power that only works one way. A component has no direction of its own, and neither does a pipeline. The words inbound and outbound describe the position a pipeline is attached at, not the artifact, and the typing rule is the same at every position. See Components classify the Message Type in the Pipelines article for the full contract; the code-level mechanics live in this reference.

Pipeline components are Application-scoped. A component belongs to exactly one Application, the same as pipelines, maps and every other artifact. Any pipeline in that Application can reference it by Name, and editing a component's code propagates to every pipeline that uses it inside that Application and nowhere else. There is no shared component library and no cross-Application reuse: if two Applications need the same processing, create the component in each.
APPLICATION LEVEL: EVERY COMPONENT BELONGS TO ONE APPLICATION APPLICATION A COMPONENTS fnVerifySig verify fnClassify set type PIPELINES PIPELINE: INBOUND_ORDERS fnVerifySig → fnClassify PIPELINE: PARTNER_ACK fnClassify APPLICATION B COMPONENTS fnClassify own copy, own code fnCompress transform PIPELINES PIPELINE: PARTNER_IN fnClassify PIPELINE: PARTNER_OUT fnCompress APPLICATION C COMPONENTS fnSplit fan-out, types each part fnRedact transform PIPELINES PIPELINE: HR_FEED fnSplit → fnRedact PIPELINE: HR_DROP fnRedact Nothing crosses an Application boundary. fnClassify exists in Application A and again in Application B, as two separate artifacts that happen to share a name. Editing one leaves the other untouched.

The list view follows the platform's Selected Application behavior, with an Application selected, the list filters to it and the Application column is hidden; with no selection, the list spans every Application you have access to and the column is shown. Creating a new component uses the same flow: with an Application selected, the editor opens with that Application preemptively chosen; without one, the form exposes an Application picker. Components are managed by the Application Owner and the Application Contributor, the same permission that governs every other artifact in the Application, no system-level role is involved.


QUICK START: FIVE STEPS 1 From template scaffold the component 2 Define config UI input fields 3 ExecuteAsync your logic here 4 Save AUTO-COMPILED 5 Reference add to pipeline USINGS · CONFIG · TRY/CATCH Start from the supplied scaffold, don’t redefine domain models. PUBLIC PROPERTIES Each property becomes a field in the UI. Add defaults inline. READ · TRANSFORM · RETURN Read input.Body, apply your work, return one or many PipelineMessages. NO BUILD · NO RESTART Art2link ESB compiles and deploys on save. No downtime. REFERENCED BY NAME Add by Name from a pipeline in the same App; attach pipeline to a port. The whole loop is edit → save → next message uses the new code. No deploy step.

The following classes are provided by the Art2link runtime in the CC.Art2link.Pipelines.Domain.Models.PipelineComponents namespace. Do not redefine them in your component code.

EXECUTEASYNC CONTRACT PipelineComponentInput string Body never null, defaults to "" PLUS: HANDED IN BY ENGINE: TConfig config CancellationToken token YOUR OVERRIDE ExecuteAsync protected override Task<PipelineComponentOutput> read input.Body, transform, return one or many messages PipelineComponentOutput bool Success IList<PipelineMessage> Messages IDictionary<string,string> Variables (writes back to ESB Variables) string? ErrorMessage Exception? EACH ITEM: PipelineMessage string Body string MessageType (required on every message, at every position) YOU INHERIT FROM PipelineComponentBase<TConfig> The base handles JSON deserialization of configJson into TConfig, then calls your override above. Empty Messages list = filtered out. Multiple Messages items = fan-out. Success = false = pipeline halts.
PipelineComponentInput

The incoming message passed to your component.

PropertyTypeDescription
BodystringThe message body. Always a string, never null at runtime (defaults to empty string).
PipelineComponentOutput

The result returned by your component after processing.

PropertyTypeDescription
SuccessboolWhether the component executed successfully. When false, the pipeline stops immediately.
MessagesIList<PipelineMessage>One or more outgoing messages. Return multiple items for fan-out, or an empty list to suppress/filter. Every item you return must carry a MessageType: a component that splits an interchange into 200 messages types all 200 of them.
VariablesIDictionary<string, string>Assignments written back to Art2link ESB Variables for the current flow instance. Keys are variable names; values replace whatever the variable currently holds. Visible to downstream components, maps, and ports via {{Variable.Name}}.
ErrorMessagestring?A human-readable error description. Set when Success is false.
ExceptionException?The caught exception, if any. Set when Success is false.
PipelineMessage

An individual outgoing message.

PropertyTypeDescription
BodystringThe outgoing message body.
MessageTypestringThe Message Type assigned to this message. Mandatory on every message a component emits, at every position in a flow, inbound or outbound. Set it even when the type is identical to the one the message arrived with: the component restates the type explicitly rather than letting it be inherited. Must match a Message Type defined in the Application: a type that is not defined there fails the run.
PipelineComponentBase<TConfig>

The abstract base class your component must inherit from. It handles JSON deserialization of the configuration and delegates to your ExecuteAsync implementation.

How config deserialization works: The pipeline engine passes configuration as a raw JSON string. The base class deserializes it into your TConfig type using System.Text.Json with case-insensitive property matching. If the JSON is empty or null, a default instance of TConfig is created using its parameterless constructor.
C#
public abstract class PipelineComponentBase<TConfig>
    : IPipelineComponent where TConfig : new()
{
    public abstract string Name { get; }

    // Called by the engine, deserializes configJson, then delegates below
    public Task<PipelineComponentOutput> ExecuteAsync(
        PipelineComponentInput input,
        string configJson,
        CancellationToken cancellationToken = default) { ... }

    // Implement this method with your logic
    protected abstract Task<PipelineComponentOutput> ExecuteAsync(
        PipelineComponentInput input,
        TConfig config,
        CancellationToken cancellationToken);
}

The configuration class defines all user-editable parameters for your component. Each public property becomes an input field in the Art2link ESB UI when the component is added to a pipeline.

Supported Property Types

Any type that is JSON-deserializable by System.Text.Json is supported: string, int, bool, double, enum, List<T>, and complex objects.

Enum Properties Render as a Dropdown

When a configuration property is typed as an enum, Art2link ESB renders it as a dropdown in the pipeline UI instead of a free-text box. The options are the enum members, and the property’s default value is preselected. The operator picks from a fixed set rather than typing a string you then have to validate, which removes a whole class of typos.

Decorate the enum with [JsonConverter(typeof(JsonStringEnumConverter))] so the selected value round-trips by name. Without it, System.Text.Json serializes the enum by its integer value, which is harder to read in stored config and in {{ }} bindings.

C#
using System.Text.Json.Serialization;

// Serializes by name ("Gzip" / "Deflate") rather than by integer
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum CompressionMode { Gzip, Deflate }

public sealed class CompressorConfig
{
    // Renders as a two-option dropdown; Gzip is preselected
    public CompressionMode Mode { get; set; } = CompressionMode.Gzip;
}
i
Inside ExecuteAsync, read the resolved selection straight off config.Mode and branch on it with a switch or if. The base class has already deserialized the operator’s choice into the enum for you, no string parsing required.
Default Values

Assign defaults directly on properties. These values are shown pre-filled in the UI when a user configures your component.

WHERE YOUR PROPERTY’S VALUE CAN BE SET: WEAKEST → STRONGEST TIER 1: COMPONENT DEFAULT In your C# code Assigned on the property: public int MaxRetries { get; set; } = 3; TIER 2: PIPELINE DEFAULT Set in the UI When the component is added to a pipeline. Inherited by every port that references that pipeline. MaxRetries = 5 TIER 3: PORT OVERRIDE Wins at runtime Edited on the port that uses the pipeline. Scoped to that one port, everything else still sees Tier 2. MaxRetries = 10 Your job is to set sensible Tier 1 values. The operator wires Tier 2 and Tier 3, see Pipelines, Defaults.
Validation

You may use attributes from the System.ComponentModel.DataAnnotations namespace to enforce constraints on configuration values.

C#
using System.ComponentModel.DataAnnotations;

public sealed class MyComponentConfig
{
    [Required]
    public string TargetEndpoint { get; set; } = string.Empty;

    [Range(1, 10)]
    public int MaxRetries { get; set; } = 3;

    public bool IncludeHeaders { get; set; } = true;
}
Runtime Binding with {{ }}

Configuration values support the {{ }} binding syntax, letting users reference application Variables, application Constants, message-level promoted properties, custom functions, and the originating port name when configuring a component in the UI:

TokenResolves to
{{Variable.Name}}The value the variable holds for the current flow instance, shared by everything derived from one arrival.
{{Constant.Name}}Application constant for the current deployment, one value per environment.
{{Promoted.MessageType.Name}}Schema-promoted property lifted from the message body.
{{Function.fnName}}A custom function evaluated at bind time.
{{Config.PortName}}The Name of the port the current message originated from. In a send-port pipeline that is the receive port that published the message, not the port the pipeline is attached to.
How binding works: Binding expressions are resolved at runtime before your component executes. Your code receives the final resolved values, it never sees the {{ }} tokens. This is configured post-deployment through the Art2link ESB UI, not in your code.

A pipeline component transforms the message it is given. Anything that varies from one message to the next is read from that message, input.Body, not carried in the component’s code or baked into a configuration default. The same compiled component should process a message from any partner, account, or document without an edit, even structural descriptors, like the transaction-set ID a 997 acknowledges, are read from the message rather than typed.

Configuration properties are for stable, deployment-level knobs, a delimiter set, a feature toggle, the name of a downstream message type, a timeout, values that are the same for every message and that an operator might tune per environment. Per-message data is read from input.Body instead. A config property’s default should be empty or neutral rather than a real value, so a missed setting fails loudly instead of running with the wrong one.

The test is reuse across partners: deploy the component once in its Application, point two partners at it, and it does the right thing for both because it reads who they are from the message. Reuse here means many partners and many messages through one component, not one component shared between Applications, which is not a thing the platform does.

Each component must override the Name property with a unique identifier. This name is used internally by the pipeline engine to locate and invoke your component.

RuleDetails
UniquenessMust be unique within the Application the component belongs to. Two Applications may each hold a component with the same name; they are separate artifacts with separate code.
PrefixNo required prefix. The fn convention (e.g., fnMyComponent) is optional.
RestrictionsNo enforced casing, character, or length restrictions.
Config class namingNo enforced naming convention between the config class and the component class.
⚠️
Changing the Name after deployment will break any pipeline that references this component. Treat it as an immutable identifier once in use. If you need to rename, create a new component and migrate pipelines manually.

Components must not throw exceptions. Instead, catch all errors and return a PipelineComponentOutput with Success = false, a descriptive ErrorMessage, and optionally the caught Exception.

When a component returns Success = false, the pipeline stops execution immediately. No subsequent components will be invoked.

C#
protected override Task<PipelineComponentOutput> ExecuteAsync(
    PipelineComponentInput input,
    MyComponentConfig config,
    CancellationToken cancellationToken)
{
    try
    {
        // Your logic here
        return Task.FromResult(new PipelineComponentOutput
        {
            Success = true,
            Messages =
            [
                new PipelineMessage
                {
                    Body        = result,
                    MessageType = "MyMessageType"   // required on every message
                }
            ]
        });
    }
    catch (Exception ex)
    {
        return Task.FromResult(new PipelineComponentOutput
        {
            Success = false,
            ErrorMessage = ex.Message,
            Exception = ex
        });
    }
}

The shape of the Messages list governs what happens next. Two patterns matter:

FAN-OUT: ONE IN, MANY OUT Input 1 message Component splits partA partB partC NEXT COMPONENT RUNS ONCE PER ITEM SUPPRESSION: ONE IN, ZERO OUT Input 1 message Component filters out EMPTY LIST: NEXT COMPONENT GETS NOTHING RETURN SHAPE IN C# Messages = [ new PipelineMessage { Body = partA, MessageType = "Order" }, new PipelineMessage { Body = partB, MessageType = "Order" }, // … etc, every item carries a MessageType Messages = [] , message is suppressed // Success still true, pipeline doesn't halt
i
Suppression is not failure. An empty Messages list with Success = true means "the message has been deliberately filtered." The pipeline continues to the next component, but with nothing to process. Setting Success = false instead would halt the pipeline, see Handling Errors.

Beyond returning a transformed body, a component has two side-effect channels into the surrounding integration. Both are written into the result your component returns.

TWO SIDE-EFFECT CHANNELS CHANNEL 1: ON PipelineComponentOutput Variables, write back to ESB Variables = { ["lastDoc"]="INV-1001" ESB Variable {{Variable.lastDoc}} Keys are application Variable names; values replace whatever the variable holds for this message. Downstream components, maps, and ports see the new value via {{Variable.Name}}. CHANNEL 2: ON EACH PipelineMessage MessageType, on every message MessageType = "Invoice" CARRIED AS type: Invoice Required on every message the component emits, inbound or outbound. Sets the Message Type and must match a type defined in the application.

Both channels are populated at the same time you build the result. This is the canonical return shape: two outgoing messages, each one typed, and one application variable written along the way. Copy it whenever you emit messages, whatever position the pipeline is attached at.

C#
protected override Task<PipelineComponentOutput> ExecuteAsync(
    PipelineComponentInput input,
    MyComponentConfig config,
    CancellationToken cancellationToken)
{
    try
    {
        // Your logic here
        return Task.FromResult(new PipelineComponentOutput
        {
            Success = true,
            Variables =
            {
                ["lastProcessedDocument"] = "INV-1001",
            },
            Messages =
            [
                new PipelineMessage
                {
                    Body = acknowledgementResult,
                    MessageType = "AcknowledgementMessageType"
                },
                new PipelineMessage
                {
                    Body = businessResult,
                    MessageType = "BusinessMessageType"
                }
            ]
        });
    }
    catch (Exception ex)
    {
        return Task.FromResult(new PipelineComponentOutput
        {
            Success = false,
            ErrorMessage = ex.Message,
            Exception = ex
        });
    }
}
i
Set the Message Type on every message you return. It is required at every position a pipeline can be attached at, and it is required even when the answer is the type the message arrived with: the component restates the type rather than letting it carry. Classifying to a Message Type that does not exist in the Application is a separate matter and a real error: the string must match a Message Type defined in the same Application as the pipeline, or the port fails the run. Worth knowing about the reach of the rule: nothing between one component and the next reads the Message Type, so on a hop inside a pipeline the value you set is carried rather than consumed, and you set it on every message all the same, because the type left on the message when the pipeline finishes is the one the bus and the map act on. See Message types for the rules; how component classification sits alongside the port's Adapter Message Type is diagrammed in Pipelines, Components classify the Message Type.

AspectDetails
Target framework.NET 8
Available librariesBuilt-in .NET libraries. Additional NuGet packages can be added via the Art2link ESB UI (name and version), then referenced with using statements at the top of your component code. Package registrations belong to the Application, so each Application registers what its own components need. See the NuGet packages article for the Application's catalogue.
Threading modelSingle-threaded per invocation. You do not need to write thread-safe code.
CancellationTokenHonor the token passed to ExecuteAsync for any long-running or awaitable work.
Async I/OMaking HTTP calls, file access, or database queries inside ExecuteAsync is not blocked, but it is not a supported scenario. Behavior may change in future releases.
Timeouts / MemoryNot enforced at this time.
LoggingNo logging interface is available at this time. Console.Write output is not captured.
Component per fileOne component per file.

When you save a component, Art2link ESB compiles and deploys it automatically. There is no manual build step and no restart required. The new version takes effect immediately for every pipeline in the same Application that references the component, and for nothing outside it.

HOT RELOAD: SAVE-TO-LIVE PATH STEP 1 Save the component Ctrl/Cmd+S STEP 2: AUTO Art2link compiles no build step, no restart STEP 3: APPLICATION-WIDE Propagates in the App every pipeline in the App STEP 4 Next message runs new code REACHES… All pipelines in this Application Ports that use them In-flight msgs? unpredictable, see warning below
⚠️
In-flight message behavior. Saving a component overwrites the previous version immediately. If a message is currently being processed by the pipeline, the outcome depends on which step the pipeline has reached at the moment of the save. The result is unpredictable on a case-by-case basis. Deploy component changes during periods of no message activity to avoid inconsistent behavior.
Version management: Previous versions are retained. Multi-version management including rollback and side-by-side comparison is planned for a future release.

Stay data-driven

Read whatever varies per message from input.Body; reserve config for stable, deployment-level knobs; keep config defaults empty rather than real values.

Always catch exceptions

Wrap your entire ExecuteAsync body in a try/catch. Return Success = false with a meaningful ErrorMessage. Never let an exception propagate to the engine.

Choose stable Names

The Name property is an immutable contract once deployed. Use a descriptive, stable identifier. If you need to rename, create a new component and migrate pipelines manually.

Keep components focused

Each component should do one thing well. Compose complex transformations by chaining multiple components in a pipeline rather than building monolithic components.

Use configuration for variability

Move environment-specific or deployment-specific values into the config class. Combined with {{ }} binding to Constants and Variables, this makes your components reusable across environments without code changes.

Honor the CancellationToken

If your component performs any awaitable work, pass the cancellationToken through to those calls. This ensures the pipeline engine can shut down gracefully.

Deploy during quiet periods

Until multi-version management is available, save component changes when no messages are actively being processed to avoid unpredictable in-flight behavior.

Ready to build

Implement your logic in the component editor and save. Art2link ESB handles compilation and deployment automatically.