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.
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.
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.
The incoming message passed to your component.
| Property | Type | Description |
|---|---|---|
| Body | string | The message body. Always a string, never null at runtime (defaults to empty string). |
The result returned by your component after processing.
| Property | Type | Description |
|---|---|---|
| Success | bool | Whether the component executed successfully. When false, the pipeline stops immediately. |
| Messages | IList<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. |
| Variables | IDictionary<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}}. |
| ErrorMessage | string? | A human-readable error description. Set when Success is false. |
| Exception | Exception? | The caught exception, if any. Set when Success is false. |
An individual outgoing message.
| Property | Type | Description |
|---|---|---|
| Body | string | The outgoing message body. |
| MessageType | string | The 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. |
The abstract base class your component must inherit from. It handles JSON deserialization of the configuration and delegates to your ExecuteAsync implementation.
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.
Any type that is JSON-deserializable by System.Text.Json is supported: string, int, bool, double, enum, List<T>, and complex objects.
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.
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; }
Assign defaults directly on properties. These values are shown pre-filled in the UI when a user configures your component.
You may use attributes from the System.ComponentModel.DataAnnotations namespace to enforce constraints on configuration values.
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; }
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:
| Token | Resolves 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. |
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.
| Rule | Details |
|---|---|
| Uniqueness | Must 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. |
| Prefix | No required prefix. The fn convention (e.g., fnMyComponent) is optional. |
| Restrictions | No enforced casing, character, or length restrictions. |
| Config class naming | No enforced naming convention between the config class and the component class. |
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.
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:
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.
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.
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 }); } }
| Aspect | Details |
|---|---|
| Target framework | .NET 8 |
| Available libraries | Built-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 model | Single-threaded per invocation. You do not need to write thread-safe code. |
| CancellationToken | Honor the token passed to ExecuteAsync for any long-running or awaitable work. |
| Async I/O | Making 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 / Memory | Not enforced at this time. |
| Logging | No logging interface is available at this time. Console.Write output is not captured. |
| Component per file | One 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.
Read whatever varies per message from input.Body; reserve config for stable, deployment-level knobs; keep config defaults empty rather than real values.
Wrap your entire ExecuteAsync body in a try/catch. Return Success = false with a meaningful ErrorMessage. Never let an exception propagate to the engine.
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.
Each component should do one thing well. Compose complex transformations by chaining multiple components in a pipeline rather than building monolithic components.
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.
If your component performs any awaitable work, pass the cancellationToken through to those calls. This ensures the pipeline engine can shut down gracefully.
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.