How to develop a custom action


By Marian Voicu

updated about 1 month ago

You’re a developer and you can’t find everything you need to design your process? You can create your own custom action that you can reuse in any process.


  • GitHub account (you can create one at if you don’t have it)

  • Visual Studio 2019 (or above)

Step 1: Connect to your GitHub account and generate a GitHub access token, by selecting from your personal account Settings > Developer settings > Personal access tokens > Generate New Token. Make sure that read:packages option is selected:


Step 2: Make sure you have nuget.exe (NuGet Command-Line Interface (CLI) Reference ) installed on your computer and that it is added to the PATH environment variable. 

Add PROCESIO Nuget source from the command line by running the following the command from command line (in admin mode to be sure it will be processed):

nuget sources add -Name procesio-development -Source  -UserName [your Git hub user name] -Password [your API Key]

To check the outcome, go in a  Visual Studio 2019, select from the menu Tools/Options/NuGet Package Manager/Package Sources and check that the following source under whatever name you’ve used:

Alternatively, you can  go in any Visual Studio 2019 menu to Tools/Options/NuGet Package Manager/Package Sources and add the following source under whatever name you’d like: You will be asked to provide your credentials (UserName [your Git hub user name] and Password [your API Key]) the first time you try to use a package from this source.

Step 3: Create a new .Net Project of type Class Library (.Net Core 3.1) and manage its NuGet packages. To manage the NuGet packages, go to Tools → Nuget Package Manager → Manage NuGet Packages for Solution. Select your newly configured package source from the upper right corner dropdown and browse for the latest Ringhel.Procesio.Action.Core package.

Step 4: Edit the .csproj file with the following lines:

  • In the PropertyGroup tag :








  • In the Project tag :

<Target Name="IncludeDepsInPackage">


   <BuildOutputInPackage Include="$(OutDir)*" />



Step 5: Create your custom action in a new class. To do this there are only two requirements to implement for your classes to become proper custom actions:

  • Include the following namespaces in your Custom Action class definition (you can find more details here: PROCESIO/Action-Core):

    • Ringhel.Procesio.Action.Core - to implement IAction interface

    • Ringhel.Procesio.Action.Core.ActionDecorators - to use Action Decorators

    • Ringhel.Procesio.Action.Core.Utils - to use Enums that are available for the different enumerators

    • Ringhel.Procesio.Action.Core.Models -  to use OptionModel class for defining multi-values inputs

  • Make sure you inherit the IAction interface. This is mandatory because Procesio needs the Execute method in order to run your action within a flow. This is where you will add the logic of whatever behavior you need the action to have.

  • Use proper attributes for your properties to define what type and use they have within your new action. These attributes are necessary for Procesio to create the Action Template, which is like a contract used for the UI to dynamically interpret and display any type of provided action correctly within the platform.

For more details about how to develop your custom action, check the sections below.

Step 6: Build the NuGet package.

Step 7: Connect to PROCESIO and open any process. On the left toolbar, select the “Custom actions” tab and click on “Create custom action”. Give a name to the action and select an icon (optional), then upload your NuGet file. Click Save. If all validations pass, your custom action will be displayed in the toolbar, under the “Custom actions” tab.

Note that a custom action can be used for any process.

If you want to update a custom action, you need to rebuild the NuGet package with a different version number, then create it again in the process designer.

Example of class implementation

[ClassDecorator(Name = "Custom Template Action", Shape = ActionShape.Circle, 

Description = "Custom Template Action Description", Classification = Classification.cat1)]

[FEDecorator(Label = "Configuration Modal", Type = FeComponentType.Modal, 

Parent = "Config_Modal", Tab = "Input Tab")]

public class MyCustomAction : IAction


   public async Task Execute()


       //add your desired behavior he

       Output = "expected outcome";


   //add properties and helper methods and whatever else is needed for your logic;


Required Class Attributes Overview

Required Property Attributes Overview

Example of full properties implementation

[FEDecorator(Label = "FE Input property", Type = FeComponentType.Select, 

Tab = "Input Tab", Options = "MyOptionsList")]

[BEDecorator(IOProperty = Direction.Input)]

[Validator(IsRequired = false)]

public int MyInput { getset; }

[FEDecorator(Label = "FE Output property", Type = FeComponentType.Number, 

Tab = "Output Tab", DefaultValue = "2")]

[BEDecorator(IOProperty = Direction.Output)]

[Validator(IsRequired = true, Expects = ExpectedType.Number)]

public int MyOutput { getset; }

The accepted types for a property are:

  • int

  • string

  • boolean

  • double

  • DateTime

We do not accept at this point user defined types or Nullable types. The value of a property in the designer will be the DefaultValue from the FEDecorator. If that property is not required via the Validator decorator and  DefaultValue is not defined either, then we will use the default value of the corresponding data type from C# (0 for int and double, false for boolean, etc.). The same applies if the default value is deleted by the user and no other value is specified.

Description of the different property attributes usage

1. Back End property attribute used to define whether a property is of Input or Output type: 

[BEDecorator(IOProperty = Direction.Input)]


[BEDecorator(IOProperty = Direction.Output)]

2. Front End property attributes are used to set the type of component, a default value or a list of options for the values, its parent component or the tab it belongs to. 

 The way the FeComponentType attribute is set will influence the display control used in the platform: 

  1. For control type Number:

  1. For control type Text: 

  1. For control type Checkbox:

  1. For control type Radio: 

  1. For control type Select:

  1. For control type Modal: 


Besides the control type, you will need a control label using the Label attribute. 

You can also set a default value to your control using the DefaultValue attribute. Example of an input control of number type with default value 0 when not set:

[FEDecorator(Label = "FE Input property", Type = FeComponentType.Number, 

Tab = "Input Tab", DefaultValue ="0")]


The Tab attribute, from the above example, is an attribute used in the UI to group all variables associated with it.


When you have complex types like Select, Buttons or Modals that will open a new window/popup, you can use the Parent attribute to group all properties under it. The parent attribute and the parent name will be automatically associated. Also, a property can belong to either a Tab or a Parent.


[FEDecorator(Label = "FE Config property 2", Type = FeComponentType.Select, 

Parent = "Config_Modal", Options = "ConfigP2Options")]

Using FeCompomponentType.Select requires the Option attribute so that the UI populates the select control correctly. Options attribute will contain the name of the list of elements you are permitting. Example of a select control to give the users the choice of selecting a value from the defined list ConfigOptions (of name & value pairs):

[FEDecorator(Label = "FE Input property", Type = FeComponentType.Select, 

Tab = "Input Tab", Options = "ConfigOptions")]


In your code, you should create a matching named (case-sensitive) list with these desired elements. These elements must respect the case-sensitive “name” & “value” pair format due to the fact that in the UI the select control will list the name, but the value will be the one sent for execution. Please note that currently Procesio supports only primitive values for these elements. 


The way you implement the list content is up to you, but here are two ways in which the options become available in the designer: for  [FEDecorator(Options = "MyListOfOptions", ...)] we can either use a string constant:

private const string MyListOfOptions= 

 "[{\"name\":\"select option name 1\",\"value\":\"1\"},

 {\"name\":\"select option name 2\",\"value\":\"2\"},

 {\"name\":\"select option name 3\",\"value\":\"3\"}]";

or an OptionModel list: 

private IEnumerable<OptionModel> MyListOfOptions { get; } = new List<OptionModel>()


           new OptionModel(){ name = "select option name 1", value = 1 }, 

           new OptionModel(){ name = "select option name 2", value = 2 } 


The way they get displayed in the platform: 

You can add constraints to your controls by using Validator attributes to set which properties are required and their expected type.

[Validator(IsRequired = true, Expects = ExpectedTypeEmail.Number)]

To control the order in which the frontend components will be displayed, we use the OrderId and RowID properties. The OrderId applies to Tab components and it controls the order in which tabs are displayed. The RowId controls the order of the components inside a parent (a tab, modal, or side panel). All components inside a parent component should have a unique RowID, otherwise, the display order will be inconsistent. Multiple components can have the same RowId as long as they belong to different parent components. An example of usage is:

//Use OrderId to specify the order in which tabs should be displayed

[FETabDecorator(TabName = "Details", OrderId = 1)]

//Use RowId to specify the order in which components inside a parent 

//should be displayed

[FEDecorator(Label = "Value to increment", Type = FeComponentType.Number, Tab = "Details", RowId = 1)]

Using lists

To use a list inside a Custom Action, one would have to define an IEnumerable property of the required type. To this end, there are several cases where this applies:

  1. List of primitive data type

IEnumerable<int> NumericList {get; set;}

The list usage is quite straightforward in this case.

  1. List of custom data type

IEnumerable<JObject> MyCustomList {get;set;}

This list is a classic IEnumerable of objects, but their content is dynamic, receiving a JSON value as input. The action developer must know the JSON structure to work with.

  1. List of files

IEnumerable<FileModel> FileList {get; set;}

This is a list of files, where each file is sent as a data stream, accessible through the Action.Core.FileModel type. 

The FileModel type has only two properties: Name and File. The Name property holds the file name, and the File property holds the file stream which can be used to retrieve the file content.

Example in using the file stream when iterating through the File list:

foreach(var file in FileList)


_ = file.Name; // this is the file name if required.

_ = file.File; // this is the file Stream which can be used to retrieve file content.

// example of file stream use:

using System.IO.Stream fileStream = file.File;

Console.WriteLine($"File stream has length of: {fileStream.Length}");


Final observations 

Now all you need to do is to create the NuGet package for your code and upload it in PROCESIO. As a rule a Nuget package must contain only 1 action when uploading it in the platform. As of now, uploading batch actions is not supported (more actions in 1 package).

Also, in order to update your custom actions, users must first delete the initial action (if it is not used in any process yet) and re-initiate the upload process with the new updated code:

If the custom action is already in use in at least a workflow it cannot be deleted, but the users can create a new action with the updated code (e.g. Custom Action_v2.0).

Example of a fully written class that is a simple custom action that computes the sum of 2 numbers and saves it on the output:


[ClassDecorator(Name = "Custom Template Action", Shape = ActionShape.Circle, 

Description = "Custom Template Action Description", Classification = Classification.cat1)]

[FEDecorator(Label = "Configuration Modal", Type = FeComponentType.Modal, 

Parent = "Config_Modal", Tab = "Input Tab")]

public class MyCustonAction : IAction


 #region Options

 private const string ConfigP1Options = "[{\"name\":\"my name1\",\"value\":\"1\"},

 {\"name\":\"my name2\",\"value\":\"2\"},{\"name\":\"my name3\",\"value\":\"3\"}]";

 private const string ConfigP2Options = 

 "[{\"name\":\"my name3\",\"value\":\"my name value 3\"},

 {\"name\":\"my name4\",\"value\":\"my name value 4\"}]";


 #region Properties

 [FEDecorator(Label = "FE Input property", Type = FeComponentType.Select, Tab = "Input Tab", Options = "ConfigP1Options")]

 [BEDecorator(IOProperty = Direction.Input)]

 [Validator(IsRequired = false)]

 public int Input1 { getset; }

 [FEDecorator(Label = "FE Input property", Type = FeComponentType.Number, 

 Tab = "Input Tab", DefaultValue ="0")]

 [BEDecorator(IOProperty = Direction.Input)]

  [Validator(IsRequired = false)]

 public int Input2 { getset; }

 [FEDecorator(Label = "FE Output property", Type = FeComponentType.Number, 

 Tab = "Output Tab", DefaultValue = "2")]

 [BEDecorator(IOProperty = Direction.Output)]

 [Validator(IsRequired = true, Expects = ExpectedType.Number)]

 public int Output1 { getset; }

 [FEDecorator(Label = "FE Config property 2", Type = FeComponentType.Select, 

 Parent = "Config_Modal", Options = "ConfigP2Options")]

 [BEDecorator(IOProperty = Direction.Output)]

 public string OutConfig2 { getset; }


 #region Execute

 public async Task Execute()


   Output1 = Input1 + Input2;




Besides the examples provided in this document, you can also follow the given example of custom actions provided by the Procesio Core project - Custom template action.

Did this answer your question?