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:
| Element | Description |
|---|---|
[Design] Attribute | Defines the display name, description, and sorting of the node in the flow designer |
[Category] Attribute | Defines the category of the node in the designer |
[DataMember] Attribute | Marks properties that can be configured in the flow designer |
[Serializable] Attribute | The node class must be serializable |
ProcessAsync Method | The core business logic of the node, called when the node executes |
FlowItemKeys Property | Defines 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
ServiceProvidercomes from the Flow component, if you call custom services in flow nodes, you must set theAbstractionsassembly 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
- Log in to the LMES system
- Enter the Flow Configuration component
- 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:
- Drag the CustomDevice node from the left node panel to the canvas
- 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
- Connect the node and set transition conditions
- 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 Name | Description |
|---|---|
FlowItemCollection.SerialNumber | Product Serial Number/Barcode |
FlowItemCollection.OrderModel | Order Model |
FlowItemCollection.ProductModel | Product Model |
FlowItemCollection.TraceModel | Trace Model |
FlowItemCollection.FormulaApplyModel | Recipe Application Model |
FlowItemCollection.QualityResultValue | Quality 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
UserFriendlyExceptionto throw business exceptions - Logging: Use
Flow.Loggerto 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
- Set a breakpoint in the node's
ProcessAsyncmethod - Ensure the flow is set to Enabled status (Reference: Flow Management)
- Start the flow service or use the flow console to manually start the flow (Reference: Flow Execution)
- 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
flowTypeto avoid conflict with system flows.
File Sync Mechanism
- The system checks for file changes every 3 seconds
.xmlfile changes trigger a full sync of all flow definitions.pfdfile 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:
- Select the node that needs parallel branching
- In the property panel, set Outgoing Mode to
SplitAND - 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/
Flow Engine Related
- LMES Flow Engine Principle - Detailed principle of flow engine
- Static Model: Flow Definition - Understand activities, transitions, conditions
- Dynamic Model: Flow Instance - Understand flow instance lifecycle
- Flow Management - Flow creation, import, export
- Flow Design - Using visual designer
- Flow Execution - Flow startup and debugging
- Flow Log - Log analysis and troubleshooting
- Flow Patterns - Common flow design patterns
Development Related
- Backend Component Development - Backend development basics
Summary
Steps to customize flow nodes in your own plugin:
- Create Node Class: Inherit
BusinessActivitybase class inAbstractionsproject, implementProcessAsyncmethod - Add Attributes: Use
[Design],[Category],[DataMember],[Serializable]attributes - Configure References: Add
CMS.Plugin.FlowManagement.Abstractionsand other package references in .csproj - Use Node: Drag and configure node properties in the flow designer
- Test and Debug: Through flow logs and breakpoint debugging