Skip to main content
Version: Next

Flow Node Customization Guide

This document describes how to customize new flow nodes in your own plugin to extend the functionality of the LMES Flow Engine.

Detailed Principle: See LMES Flow Engine Documentation


1. Create Custom Flow Node​

1.1 Create Node Class in Your Plugin​

Create a custom node class in your plugin's Abstractions project, inheriting from the BusinessActivity base class.

Here is the complete sample code (reference doc/server/src/CMS.Plugin.MyPluginName.Abstractions):

using System.ComponentModel;
using System.Runtime.Serialization;
using CMS.Plugin.FlowManagement.Abstractions.Enums;
using CMS.Plugin.FlowManagement.Abstractions.FlowBusiness.Activitys;
using CMS.Plugin.MesSuite.Abstractions.Models;
using CMS.Plugin.OrderManagement.Abstractions.Models;
using CMS.Plugin.ProcessManagement.Abstractions.Models;
using CMS.Plugin.TraceManagement.Abstractions.Models.Traces;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SYC.Flow.Kernel;

namespace CMS.Plugin.MyPluginName.Abstractions
{
/// <summary>
/// MyPluginName Business Step
/// </summary>
[Design("MyPluginName", "MyPluginName Business Step", Sort = 99), Category("Custom Steps")]
[Serializable]
public class MyPluginNameActivity : BusinessActivity
{
/// <summary>
/// Configuration Property 1
/// </summary>
[Design("Config Property 1", "Configuration Property 1", Sort = 1), Category("Configuration Info")]
[DataMember]
public string MyProperty1 { get; set; }

/// <summary>
/// Configuration Property 2
/// </summary>
[Design("Config Property 2", "Configuration Property 2", Sort = 2), Category("Configuration Info")]
[DataMember]
public int MyProperty2 { get; set; }

/// <summary>
/// Flow Context Identifier Collection
/// </summary>
public override List<FlowItemKey> FlowItemKeys => GetFlowItemKeys();

/// <summary>
/// Process Flow Handling
/// </summary>
public override async Task ProcessAsync(ProcessflowEventArgs args)
{
// Process Model
var processModel = Flow.DataItems.ApplicationData as ProcessModel;

// Order Model
var orderModel = Flow.DataItems[FlowItemCollection.OrderModel] as OrderModel;

// Product Model
var productModel = Flow.DataItems[FlowItemCollection.ProductModel] as AssociationProductModel;

// Trace Model
var traceModel = Flow.DataItems[FlowItemCollection.TraceModel] as TraceModel;

Flow.Logger.LogInformation($"Execute Flow: Instance={Flow.Instance.ProcID} -> {Flow.Name} -> {Name}");

// Business Processing
var myPluginNameFlowService = Flow.ServiceProvider.GetService<IMyPluginNameFlowService>();
if (myPluginNameFlowService != null)
{
await myPluginNameFlowService.ProcessAsync(args);
}
}

/// <summary>
/// Gets the flow item keys.
/// </summary>
protected virtual List<FlowItemKey> GetFlowItemKeys()
{
var result = new List<FlowItemKey>();
return result;
}
}
}

Key Explanation:

ElementDescription
[Design] AttributeDefines the display name, description, and sorting of the node in the flow designer
[Category] AttributeDefines the category of the node in the designer
[DataMember] AttributeMarks properties that can be configured in the flow designer
[Serializable] AttributeThe node class must be serializable
ProcessAsync MethodThe core business logic of the node, called when the node executes
FlowItemKeys PropertyDefines the flow context keys used by the node

1.2 Project Reference Configuration​

Add the necessary references in your plugin project file (.csproj):

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<!-- Recommended: Include Abstractions references for all business plugins, version depends on the LMES version used by the project -->
<PackageReference Include="CMS.Plugin.MesSuite.Activitys" Version="x.x.x.*" />
</ItemGroup>

</Project>

Note: Minimize external dependencies. If you must reference other assemblies, you need to configure shared assemblies in the main plugin module (see Section 1.5).

1.3 Configure AssemblyInfo.cs​

Add the flow extension attribute in Properties/AssemblyInfo.cs of the Abstractions project:

using SYC.Flow.Kernel;

[assembly: ProcessflowExtention(true)]

This attribute tells the flow engine to scan this assembly for custom flow nodes.

1.4 Define Service Interface (Optional)​

If the business logic is complex, it is recommended to extract the business logic into a service:

using SYC.Flow.Kernel;

namespace CMS.Plugin.MyPluginName.Abstractions
{
/// <summary>
/// MyPluginName Flow Service
/// </summary>
public interface IMyPluginNameFlowService
{
/// <summary>
/// Process Flow Handling
/// </summary>
Task ProcessAsync(ProcessflowEventArgs args);
}
}

1.5 Configure Shared Assemblies​

Configure shared assemblies in the main plugin module (CMS.Plugin.MyPluginName/CMSPluginModule.cs):

using System.Reflection;
using CMS.Extensions.Abp;
using CMS.Plugin.MyPluginName.Abstractions;
using Volo.Abp.Modularity;

namespace CMS.Plugin.MyPluginName
{
[DependsOn(typeof(CMSPluginAbpModule))]
public class CMSPluginModule : AbpStartupModule
{
/// <inheritdoc />
public override Assembly[]? GetSharedAssemblies()
{
// Set the Abstractions assembly as a shared assembly
// So that the flow engine can correctly load custom nodes
return base.GetSharedAssemblies().Concat(new[]
{
typeof(CMSPluginMyPluginNameAbstractionsModule).Assembly,
}).ToArray();
}
}
}

Important: Since ServiceProvider comes from the Flow component, if you call custom services in flow nodes, you must set the Abstractions assembly as a shared assembly, otherwise the services cannot be resolved correctly.

1.6 Use in Flow Designer​

After registering the node, your custom node can be used in the LMES Flow Designer.

Open Flow Designer​

  1. Log in to the LMES system
  2. Enter the Flow Configuration component
  3. Select a flow or create a new flow, click the Flow Design button

For detailed flow management operations, please refer to: Flow Management

Configure Custom Node​

In the flow designer, your custom node will appear in the node panel:

  1. Drag the CustomDevice node from the left node panel to the canvas
  2. Select the node and configure the node properties in the right property panel:
    • Node Name: Send Device Command
    • DeviceAddress: 192.168.1.100
    • Command: START_PROCESS
  3. Connect the node and set transition conditions
  4. Save the flow definition

For detailed usage of the flow designer, please refer to: Flow Design


2. Common Node Development Scenarios​

2.1 Data Collection Node​

using System.ComponentModel;
using System.Runtime.Serialization;
using CMS.Plugin.FlowManagement.Abstractions.Enums;
using CMS.Plugin.FlowManagement.Abstractions.FlowBusiness.Activitys;
using CMS.Plugin.FlowManagement.Abstractions.FlowBusiness.Variables;
using Microsoft.Extensions.DependencyInjection;
using SYC.Flow.Kernel;

namespace CMS.Plugin.MyPluginName.Abstractions
{
[Design("Data Collection", "Collect data from variables", Sort = 101), Category("Custom Steps")]
[Serializable]
public class DataCollectionActivity : BusinessActivity
{
[Design("Variable Name", "Name of the variable to collect"), Category("Collection Config")]
[DataMember]
public string VariableName { get; set; }

public override List<FlowItemKey> FlowItemKeys => new List<FlowItemKey>
{
new FlowItemKey("CollectedValue", "Collected Value", "Value of the collected variable", typeof(string))
};

public override async Task ProcessAsync(ProcessflowEventArgs args)
{
if (string.IsNullOrWhiteSpace(VariableName))
{
Flow.Logger.LogWarningMessage("Variable name is empty", Name);
return;
}

// Use variable service to read variable value
var variableService = Flow.ServiceProvider.GetRequiredService<IFlowVariableService>();
var traceId = Flow.Instance.GetTraceId();
var values = await variableService.ReadValueAsync(this, new[] { VariableName }, traceId: traceId);

// Save to flow context
if (values.TryGetValue(VariableName, out var value))
{
Flow.DataItems["CollectedValue"] = value;
Flow.Logger.LogMessage($"Collect Variable: {VariableName}={value}, TraceId={traceId}", Name);
}
}
}
}

2.2 Data Validation Node​

using System.ComponentModel;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;
using CMS.Plugin.FlowManagement.Abstractions.FlowBusiness.Activitys;
using SYC.Flow.Kernel;
using Volo.Abp;

namespace CMS.Plugin.MyPluginName.Abstractions
{
[Design("Data Validation", "Validate flow data", Sort = 102), Category("Custom Steps")]
[Serializable]
public class DataValidationActivity : BusinessActivity
{
[Design("Data Key", "Flow context data key to validate"), Category("Validation Config")]
[DataMember]
public string DataKey { get; set; }

[Design("Validation Rule", "Regular expression validation rule"), Category("Validation Config")]
[DataMember]
public string ValidationRule { get; set; }

public override async Task ProcessAsync(ProcessflowEventArgs args)
{
var data = Flow.DataItems[DataKey]?.ToString();

if (string.IsNullOrWhiteSpace(data))
{
throw new UserFriendlyException($"Data validation failed: {DataKey} is empty");
}

if (!string.IsNullOrWhiteSpace(ValidationRule) && !Regex.IsMatch(data, ValidationRule))
{
throw new UserFriendlyException($"Data validation failed: {data} does not match rule {ValidationRule}");
}

Flow.DataItems["ValidationResult_Value"] = "Pass";
Flow.Logger.LogMessage($"Data validation passed: {DataKey}={data}", Name);

await Task.CompletedTask;
}
}
}

2.3 Third-Party System Integration Node​

using System.ComponentModel;
using System.Runtime.Serialization;
using CMS.Plugin.FlowManagement.Abstractions.FlowBusiness.Activitys;
using CMS.Plugin.OrderManagement.Abstractions.Models;
using CMS.Plugin.MesSuite.Abstractions.Models;
using Microsoft.Extensions.DependencyInjection;
using SYC.Flow.Kernel;
using Volo.Abp;

namespace CMS.Plugin.MyPluginName.Abstractions
{
[Design("ERP Integration", "Call ERP system interface", Sort = 103), Category("Custom Steps")]
[Serializable]
public class ErpIntegrationActivity : BusinessActivity
{
[Design("Interface Address", "ERP Interface URL"), Category("Interface Config")]
[DataMember]
public string ApiEndpoint { get; set; }

public override async Task ProcessAsync(ProcessflowEventArgs args)
{
// Get order information from flow context
var orderModel = Flow.DataItems[FlowItemCollection.OrderModel] as OrderModel;

if (orderModel == null)
{
Flow.Logger.LogWarningMessage("Order information is empty, skipping ERP sync", Name);
return;
}

// Get custom service
var erpService = Flow.ServiceProvider.GetService<IErpIntegrationService>();
if (erpService == null)
{
throw new UserFriendlyException("ERP integration service not configured");
}

try
{
var result = await erpService.SyncOrderAsync(orderModel.Code, ApiEndpoint);
Flow.DataItems["ErpSyncResult_Value"] = result;
Flow.Logger.LogMessage($"ERP sync successful: {orderModel.Code}", Name);
}
catch (Exception ex)
{
Flow.Logger.LogExceptionMessage(ex, Name);
throw new UserFriendlyException($"ERP interface call exception: {ex.Message}");
}
}
}
}

3. Flow Context Data Operations​

Flow context (Flow.DataItems) is the core mechanism for passing data between nodes.

3.1 Basic Data Operations​

// Set data
Flow.DataItems["MyData_Value"] = "P001";
Flow.DataItems["Quantity_Value"] = 100;

// Get data
var myData = Flow.DataItems["MyData_Value"]?.ToString();
var quantity = Convert.ToInt32(Flow.DataItems["Quantity_Value"]);

// Check if data exists
if (Flow.DataItems.ContainsKey("MyData_Value"))
{
// Data exists
}

3.2 Get Common Models​

LMES pre-populates common business models in the flow context:

// Process Model (contains operation, workstation configuration)
var processModel = Flow.DataItems.ApplicationData as ProcessModel;
var workSectionName = processModel?.WorkSection?.Name;
var workStationName = processModel?.WorkStation?.Name;

// Order Model
var orderModel = Flow.DataItems[FlowItemCollection.OrderModel] as OrderModel;

// Product Model
var productModel = Flow.DataItems[FlowItemCollection.ProductModel] as AssociationProductModel;

// Trace Model
var traceModel = Flow.DataItems[FlowItemCollection.TraceModel] as TraceModel;

3.3 Common FlowItemCollection Keys​

The FlowItemCollection class defines common flow context keys:

Key NameDescription
FlowItemCollection.SerialNumberProduct Serial Number/Barcode
FlowItemCollection.OrderModelOrder Model
FlowItemCollection.ProductModelProduct Model
FlowItemCollection.TraceModelTrace Model
FlowItemCollection.FormulaApplyModelRecipe Application Model
FlowItemCollection.QualityResultValueQuality Judgment Result

3.4 Use Services and Logs​

// Get injected services
var variableService = Flow.ServiceProvider.GetRequiredService<IFlowVariableService>();
var orderProvider = Flow.ServiceProvider.GetRequiredService<IOrderProvider>();

// Get TraceId (for log tracking)
var traceId = Flow.Instance.GetTraceId();

// Log messages
Flow.Logger.LogMessage("Info Log", Name);
Flow.Logger.LogWarningMessage("Warning Log", Name);
Flow.Logger.LogErrorMessage("Error Log", Name);
Flow.Logger.LogExceptionMessage(exception, Name);

4. Best Practices​

4.1 Node Design Principles​

  • Single Responsibility: Each node does only one thing
  • Configurability: Expose configurable properties via [Design] and [DataMember] attributes
  • Exception Handling: Use UserFriendlyException to throw business exceptions
  • Logging: Use Flow.Logger to record key operations

4.2 Use Dependency Injection​

Get services via Flow.ServiceProvider:

[Design("Custom Node", "Node using dependency injection", Sort = 104), Category("Custom Steps")]
[Serializable]
public class CustomActivity : BusinessActivity
{
public override async Task ProcessAsync(ProcessflowEventArgs args)
{
// Get services
var orderProvider = Flow.ServiceProvider.GetRequiredService<IOrderProvider>();
var productProvider = Flow.ServiceProvider.GetRequiredService<IProductProvider>();

// Use services
var order = await orderProvider.GetCurrentAsync();
Flow.Logger.LogMessage($"Current Order: {order?.Code}", Name);
}
}

4.3 Exception Handling​

public override async Task ProcessAsync(ProcessflowEventArgs args)
{
try
{
// Business Logic
await DoBusinessLogicAsync();
}
catch (UserFriendlyException)
{
// Business exception, throw directly, flow will be interrupted and show error message
throw;
}
catch (Exception ex)
{
// Record exception log
Flow.Logger.LogExceptionMessage(ex, Name);

// Convert to business exception
throw new UserFriendlyException($"Node execution failed: {ex.Message}");
}
}

5. Debugging and Testing​

5.1 Local Debugging​

Start Flow for Debugging​

  1. Set a breakpoint in the node's ProcessAsync method
  2. Ensure the flow is set to Enabled status (Reference: Flow Management)
  3. Start the flow service or use the flow console to manually start the flow (Reference: Flow Execution)
  4. Trigger flow execution and observe if the breakpoint is hit

View Flow Logs​

Flow execution logs are stored in the logs/flow_logs/{Operation_Workstation}/ directory:

  • flowlog_xxxx-xx-xx.log: Flow instance execution log
  • tracelog_xxxx-xx-xx.log: Variable trace log

For detailed log analysis methods, please refer to: Flow Log

Initialize Flow via File​

Custom flow files should be placed in the Flows directory of the Host program. The system will automatically scan and sync to the database when it starts.

Note: Do not place custom flows in the LMES plugin directory (src/cms.plugin.messuite/Flows), as that directory only stores system built-in flows.

File Location

{Host}/
└── Flows/
├── FlowInfo/ # Flow info configuration directory
│ └── 20001.xml # Custom flow detailed configuration
└── FlowPfd/ # Flow definition file directory
└── 20001_1.pfd # Custom flow definition file

FlowInfo/{flowType}.xml Example

<?xml version="1.0" encoding="utf-8"?>
<Root>
<FlowInfo>
<Type>20001</Type>
<Name>My Custom Flow</Name>
<Version>1</Version>
<FilePath>FlowPfd/20001_1.pfd</FilePath>
<BusinessType>
<Name>Standard</Name>
<Value>1</Value>
<Description>Inbound/Outbound Interaction</Description>
</BusinessType>
<IsStatic>True</IsStatic>
</FlowInfo>
</Root>

File Naming Rules

  • Flow Info File: FlowInfo/\{flowType\}.xml, e.g., FlowInfo/20001.xml
  • Flow Definition File: FlowPfd/\{flowType\}_\{flowVersion\}.pfd, e.g., FlowPfd/20001_1.pfd

Suggestion: Use numbers 20000-79999 for custom flow flowType to avoid conflict with system flows.

File Sync Mechanism

  • The system checks for file changes every 3 seconds
  • .xml file changes trigger a full sync of all flow definitions
  • .pfd file changes only update the content of the corresponding flow definition version
  • Flow definition cache is automatically cleared after sync

5.2 Unit Testing​

Since BusinessActivity depends on the flow engine runtime environment, it is recommended to debug through integration tests or in actual flows.

If you need to unit test business logic, you can extract the core logic into an independent service class:

// Extract business logic to service class
public class DeviceCommandService : IDeviceCommandService
{
public async Task<string> SendCommandAsync(string address, string command)
{
// Business Logic
return "Success";
}
}

// Call service in node
public override async Task ProcessAsync(ProcessflowEventArgs args)
{
var service = Flow.ServiceProvider.GetRequiredService<IDeviceCommandService>();
var result = await service.SendCommandAsync(DeviceAddress, Command);
Flow.DataItems["Result_Value"] = result;
}

// Unit test service class
public class DeviceCommandServiceTests
{
[Fact]
public async Task SendCommand_Should_Return_Success()
{
var service = new DeviceCommandService();
var result = await service.SendCommandAsync("192.168.1.100", "START");
Assert.Equal("Success", result);
}
}

6. FAQ​

Q1: How to implement conditional branching in a flow?

Configure transition conditions in the flow designer, using Property Conditions to judge flow context data:

  • In the flow designer, select the connection line (Transition)
  • Configure the condition in the property panel:
    • Parameter Name: Quality (Data key in flow context)
    • Operator: Equal
    • Constant Value: OK

For detailed condition configuration, please refer to: Property Condition

Q2: How to implement parallel flows?

Set the node's outgoing mode in the flow designer:

  1. Select the node that needs parallel branching
  2. In the property panel, set Outgoing Mode to SplitAND
  3. Connect multiple subsequent nodes, these nodes will execute in parallel

For detailed incoming/outgoing modes, please refer to: Incoming/Outgoing

Q3: Which NuGet packages do custom nodes need to reference?

<!-- Recommended: Include Abstractions references for all business plugins, version depends on the LMES version used by the project -->
<PackageReference Include="CMS.Plugin.MesSuite.Activitys" Version="x.x.x.*" />

7. Learning Resources​

Sample Code​

Complete flow node customization sample code is located at: doc/server/src/CMS.Plugin.MyPluginName.Abstractions/


Summary​

Steps to customize flow nodes in your own plugin:

  1. Create Node Class: Inherit BusinessActivity base class in Abstractions project, implement ProcessAsync method
  2. Add Attributes: Use [Design], [Category], [DataMember], [Serializable] attributes
  3. Configure References: Add CMS.Plugin.FlowManagement.Abstractions and other package references in .csproj
  4. Use Node: Drag and configure node properties in the flow designer
  5. Test and Debug: Through flow logs and breakpoint debugging