Skip to main content

Introducing Formengine - The New Formbuilder, try for FREE formengine.io.

How to integrate

info

Also check out the integration tips section.

An integrated project is included at the end of this article. Furthermore, it is possible to download our samples and see how to manage workflows in web applications.

Workflow Engine scheme

#ComponentsDescriptionDocumentation
1WFE DesignerIt is a javascript object on a web page where you want the designer to be displayed, and a function that will process requests from the designer and pass them to WorkflowRuntime.Designer
2WFE RuntimeThis object allows to create a process, retrieve a list of commands, execute commands, set a process state, etc. It also provides an API for the designer, so you have to create one WorkflowRuntime object in your application or service and call its methods to enable document management functionality.Runtime, Basic Operations
3DB ProviderYou have to use persistence providers to connect to the process database. Depending on your database type, you have to create an object and pass it to WorkflowRuntime.Persistence
4Workflow tablesA set of database tables. It's needed for workflow storage.Persistence
5IWorkflowRuleProviderProvides for security integration. It allows you to call the available authorization methods, or write new ones. Rules represent the functions that are called at a certain moment in time.Rules
6IWorkflowActionProviderProvides for business logic integration. It allows you to call your own business functions for each step of the workflow.Actions

This article describes an example for ASP.NET Core MVC web application and MS SQL database.

Integration with solutions based on other technologies (ASP.NET WebForms, .NET WinForms) or databases (MySQL, PostgreSQL, Oracle, MongoDB and others) is roughly the same and should not entail any difficulties.

Integrating Workflow Engine into an application takes about an hour and consists of 6 simple steps:

  1. Creating an empty solution in Visual Studio or JetBrains Rider.
  2. Setting up the database.
  3. Initializing WorkflowRuntime.
  4. Connecting the Designer.
  5. Creating a document workflow scheme.
  6. Creating a process and calling commands.

If you plan to integrate Workflow Designer into an Angular-based or React-based application, read the following guide.

0. Video tutorial

1. Creating an empty solution

Firstly, it will be required to create an empty solution where will be created a class library project (WorkflowLib) and an ASP.NET Core MVC web application (Model-View-Controller) i.e. the Workflow Designer project. In the preferred IDE, you should choose 'New Solution' -> ' EmptySolution'.

2. Setting up the database

Download our .NET Core persistence provider here. Moreover, you might download providers from GitHub repository.

CreatePersistenceObjects

info

Learn more about persistence.

3. Initializing WorkflowRuntime

3.1. Create a Class Library project in Visual Studio or JetBrains Rider. It can be called: WorkflowLib.

Remember

Starting with WFE 7.0, the artifacts for the .NET Framework are no longer available. The .NET Framework 4.6.2 is now the minimum supported version (netstandard2.0), except for Oracle Provider, for which the minimum version is .NET Core 3.0 (netstandard2.1).

3.2. Add the following NuGet packages into your project (.NET Framework / .NET Core):

Once NuGet packets are installed, assemblies will be added to the project.

3.3. Add reference System.Configuration (for .NET Framework only).

3.4. Create a WorkflowInit.cs file. Add the following namespaces to the usings section:

using System;
using System.Xml.Linq;
using OptimaJet.Workflow.Core.Builder;
using OptimaJet.Workflow.Core.Bus;
using OptimaJet.Workflow.Core.Runtime;
using OptimaJet.Workflow.DbPersistence;

Add the initialization of the WorkflowRuntime object. It should look like this:

public static class WorkflowInit
{
private static readonly Lazy<WorkflowRuntime> LazyRuntime = new Lazy<WorkflowRuntime>(InitWorkflowRuntime);

public static WorkflowRuntime Runtime
{
get { return LazyRuntime.Value; }
}

public static string ConnectionString { get; set; }

private static WorkflowRuntime InitWorkflowRuntime()
{
// TODO Uncomment for .NET Framework if you don't set ConnectionString externally.
//ConnectionString = System.Configuration.ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString;

if (string.IsNullOrEmpty(ConnectionString))
{
throw new Exception("Please init ConnectionString before calling the Runtime!");
}
// TODO If you have a license key, you have to register it here
//WorkflowRuntime.RegisterLicense("your license key text");

// TODO If you are using database different from SQL Server you have to use different persistence provider here.
var dbProvider = new MSSQLProvider(ConnectionString);

var builder = new WorkflowBuilder<XElement>(
dbProvider,
new OptimaJet.Workflow.Core.Parser.XmlWorkflowParser(),
dbProvider
).WithDefaultCache();

var runtime = new WorkflowRuntime()
.WithBuilder(builder)
.WithPersistenceProvider(dbProvider)
.RunMigrations()
.EnableCodeActions()
.SwitchAutoUpdateSchemeBeforeGetAvailableCommandsOn()
.AsSingleServer();

var plugin = new OptimaJet.Workflow.Plugins.BasicPlugin();
// Settings for SendEmail actions
// plugin.Setting_Mailserver = "smtp.yourserver.com";
// plugin.Setting_MailserverPort = 25;
// plugin.Setting_MailserverFrom = "from@yourserver.com";
// plugin.Setting_MailserverLogin = "login@yourserver.com";
// plugin.Setting_MailserverPassword = "pass";
// plugin.Setting_MailserverSsl = true;
runtime.WithPlugin(plugin);

// events subscription
runtime.ProcessActivityChanged += (sender, args) => { };
runtime.ProcessStatusChanged += (sender, args) => { };
// TODO If you have planned to use Code Actions functionality that required references to external assemblies
// you have to register them here
//runtime.RegisterAssemblyForCodeActions(Assembly.GetAssembly(typeof(SomeTypeFromMyAssembly)));

// starts the WorkflowRuntime
// TODO If you have planned use Timers the best way to start WorkflowRuntime is somewhere outside
// of this function in Global.asax for example
runtime.Start();

return runtime;
}
}

If you have a license key, add a call to the WorkflowRuntime.RegisterLicense method with the license key.

info

You can find more information in this documentation section.

4. Connecting the Designer

info

Starting with Workflow Engine 13.0, you can quickly launch the designer using the npx command. To do so, enter the following command in your terminal:

npx @optimajet/workflow-designer http:localhost:5000/Designer/API Scheme
  • First parameter: The address of your backend server that hosts the workflow engine. By default, this is set to https://demo.workflowengine.io/Designer/API.
  • Second parameter: The name of the schema to connect to. The default is SimpleWF.
Remember

Starting with WFE 7.0, the artifacts for the .NET Framework are no longer available. The .NET Framework 4.6.2 is now the minimum supported version (netstandard2.0), except for Oracle Provider, for which the minimum version is .NET Core 3.0 (netstandard2.1).

4.1. Create an ASP.NET Core MVC web application (choose Model-View-Controller). You just should create a project according to the dotnet template. This project can be called WorkflowDesigner. The instructions from Microsoft are the following:

4.2. Add the following NuGet packages to the project (.NET Framework / .NET Core):

4.3. The reference to the class library project: WorkflowLib from step 3, must be added to have access to the WorkflowRuntime object. You can connect the dependency as indicated below:

Connecting Designer

4.4 Then, create a new controller DesignerController which will handle the designer's requests to the backend.

using System.Collections.Specialized;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using OptimaJet.Workflow;
using WorkflowLib;

namespace WorkflowDesigner.Controllers
{
public class DesignerController : Controller
{
public async Task<IActionResult> Api()
{
Stream? filestream = null;
var parameters = new NameValueCollection();

//Defining the request method
var isPost = Request.Method.Equals("POST", StringComparison.OrdinalIgnoreCase);

//Parse the parameters in the query string
foreach (var q in Request.Query)
{
parameters.Add(q.Key, q.Value.First());
}

if (isPost)
{
//Parsing the parameters passed in the form
var keys = parameters.AllKeys;

foreach (var key in Request.Form.Keys)
{
if (!keys.Contains(key))
{
parameters.Add(key, Request.Form[key]);
}
}

//If a file is passed
if (Request.Form.Files.Count > 0)
{
//Save file
filestream = Request.Form.Files[0].OpenReadStream();
}
}

//Calling the Designer Api and store answer
var (result, hasError) = await WorkflowInit.Runtime.DesignerAPIAsync(parameters, filestream);

//If it returns a file, send the response in a special way
if (parameters["operation"]?.ToLower() == "downloadscheme" && !hasError)
return File(Encoding.UTF8.GetBytes(result), "text/xml");

if (parameters["operation"]?.ToLower() == "downloadschemebpmn" && !hasError)
return File(Encoding.UTF8.GetBytes(result), "text/xml");

//response
return Content(result);
}
}
}

In addition, the following method must be appended to the created DesignerController:

public IActionResult Index()
{
return View();
}

4.5 Next, the directory called 'Designer' and a html page: Index.cshtml, which the Designer will be opened, must be created. It should be located in the folder 'Views' according the ASP NET Core architecture approach. Furthermore, the link on the main page to access the Designer should be included in the page: Index.cshtml in 'Home' directory.

  • In Designer directory the following code should be added in Index.cshtml :
@{
ViewBag.Title = "Designer";
Layout = "~/Views/Shared/_Layout.cshtml";
}

Connecting Designer

  • Moreover, in Index.cshtml in Home directory this code:
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p><a href="/Designer/Index">Open designer here</a></p>
</div>

Connecting Designer

4.6 Download the frontend artifacts for the Designer to the directory with static content: /wwwroot. They can be found in the WorkflowEngine.NET site. The folder 'templates' is required, besides the file: workflowdesigner.min.css and workflowdesigner.min.js (save them in 'css' and 'js' directories respectively).

4.7. The ConnectionString should be annexed to the settings and use it at the start of the application. This is required for the Designer API to work. Then, add a database connection string in the ConnectionStrings section in web.config or appsettings.json configuration file.

  • For .NET Framework:
<connectionStrings>
<add name="ConnectionString"
connectionString="Data Source=(local);Initial Catalog=WFE;Integrated Security=False;User ID=sa;Password=1"
providerName="System.Data.SqlClient" />
</connectionStrings>
  • For .NET Core:
{ 
"ConnectionStrings": {
"DefaultConnection": "Data Source=(local);Initial Catalog=wfe_sample;User ID=sa;Password=1"
}
}
Be aware

Make sure that server address, authentication type, login and password match your settings.

Furthermore, the ConnectionString initialization for WorkflowRuntime is needed.

  • In case of .NET Framework: Check that the following code in InitWorkflowRuntime method from WorkflowLib project in WorkflowInit.cs is uncommented:
ConnectionString = System.Configuration.ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString;
  • For .NET Core: Insert the following code in Program.cs:
using WorkflowLib;
...

//Defining connection settings
WorkflowInit.ConnectionString = app.Configuration.GetConnectionString("DefaultConnection");

4.8 The main Designer frontend functions must be attached, so a render of the designer to the designer's view should be in Index.cshtml in Designer directory. The following actions are required:

  • Include the links to the files in the folder with static content (the Jquery library is already in the project folder 'template').
  • Add the <div> section where the designer will be placed.
  • Add also a <script> section for rendering the designer. A designer object will be created in it, which will send requests to the API, receive data for rendering and display it.
<link href="~/css/workflowdesigner.min.css" rel="stylesheet" type="text/css" />
<script src="~/js/workflowdesigner.min.js" type="text/javascript"></script>
<script src="~/lib/jquery/dist/jquery.min.js" type="text/javascript"></script>

<div id="wfdesigner" style="min-height:600px; max-width: 1200px;"></div>

<script>
var QueryString = function () {
// This function is anonymous, is executed immediately and
// the return value is assigned to QueryString!
var query_string = {};
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split("=");
// If first entry with this name
if (typeof query_string[pair[0]] === "undefined") {
query_string[pair[0]] = pair[1];
// If second entry with this name
} else if (typeof query_string[pair[0]] === "string") {
var arr = [query_string[pair[0]], pair[1]];
query_string[pair[0]] = arr;
// If third or later entry with this name
} else {
query_string[pair[0]].push(pair[1]);
}
}
return query_string;
}();
//Load settings
var schemecode = QueryString.code ? QueryString.code : 'SimpleWF';
var processid = QueryString.processid;
var graphwidth = 1200;
var graphminheight = 600;
var graphheight = graphminheight;
var wfdesigner = undefined;

//Recreate designer object
function wfdesignerRedraw() {
var data;
if (wfdesigner != undefined) {
wfdesigner.destroy();
}
wfdesigner = new WorkflowDesigner({
name: 'simpledesigner',
apiurl: '/Designer/API',
renderTo: 'wfdesigner',
templatefolder: '/templates/',
graphwidth: graphwidth,
graphheight: graphheight
});
if (data == undefined) {
var isreadonly = false;
if (processid != undefined && processid != '')
isreadonly = true;
var p = { schemecode: schemecode, processid: processid, readonly: isreadonly };
if (wfdesigner.exists(p))
wfdesigner.load(p);
else
wfdesigner.create(schemecode);
}
else {
wfdesigner.data = data;
wfdesigner.render();
}
}
wfdesignerRedraw();
//Adjusts the size of the designer window
$(window).resize(function () {
if (window.wfResizeTimer) {
clearTimeout(window.wfResizeTimer);
window.wfResizeTimer = undefined;
}
window.wfResizeTimer = setTimeout(function () {
var w = $(window).width();
var h = $(window).height();
if (w > 300)
graphwidth = w - 40;
if (h > 300)
graphheight = h - 250;
if (graphheight < graphminheight)
graphheight = graphminheight;
wfdesigner.resize(graphwidth, graphheight);
}, 150);
});
$(window).resize();

</script>

4.9 The schema management functions or control functions should be inserted as well, so the next steps are recommended:

  • Above the <div> section which was placed the Designer object in Index.cshtml in the Designer directory, must be added a <form> section where buttons that are tied to functions, will be drawn in workflowdesigner.min.js.
...
<form action="" id="uploadform" method="post" enctype="multipart/form-data" onsubmit="tmp()" style="padding-bottom: 8px;">
<input type="file" name="uploadFile" id="uploadFile" style="display:none" onchange="javascript: UploadScheme(this);">
</form>

<div id="wfdesigner" style="min-height:600px; max-width: 1200px;"></div>
...
  • Next, add a function to each button in Index.cshtml also. The names of the functions must indicate what they do.
<script>
...

$(window).resize();
//Add Control functions

function DownloadScheme() {
wfdesigner.downloadscheme();
}
function DownloadSchemeBPMN() {
wfdesigner.downloadschemeBPMN();
}
var selectSchemeType;
function SelectScheme(type) {
if (type)
selectSchemeType = type;
var file = $('#uploadFile');
file.trigger('click');
}
function UploadScheme(form) {
if (form.value=="")
return;
if (selectSchemeType == "bpmn") {
wfdesigner.uploadschemeBPMN($('#uploadform')[0], function () {
wfdesigner.autoarrangement();
alert('The file is uploaded!');
});
}
else {
wfdesigner.uploadscheme($('#uploadform')[0], function () {
alert('The file is uploaded!');
});
}
}
function OnSave() {
wfdesigner.schemecode = schemecode;
var err = wfdesigner.validate();
if (err != undefined && err.length > 0) {
alert(err);
}
else {
wfdesigner.save(function () {
alert('The scheme is saved!');
});
}
}

function OnNew() {
wfdesigner.create();
}
</script>
  • Then, styles can be set to make the buttons look pretty in site.css in the directory where frontend artifacts are located in -> /wwwroot/css/. Probably, you will need to edit the styles of the application and the Designer size.
body {
margin-bottom: 60px;
}

a {
outline: none;
text-decoration: none;
}

.ui {
padding: 8px 15px;
}

.ui.primary.button,
.ui.primary.button:focus {
background: #f2f2f2;
border: 1px solid #f2f2f2;
border-radius: 2px;
font-weight: normal;
color: #2c2c2c;
}

.ui.primary.button:hover {
background: #c9c9c9;
}

.ui.primary.button:active {
background: #b5b5b5;
}

.ui.secondary.button,
.ui.secondary.button:focus {
background: #FFFFFF;
border: 1px solid #f2f2f2;
border-radius: 2px;
font-weight: normal;
color: #4d4d4d;
}

.ui.secondary.button:hover {
background: #dbdbdb;
color: #2c2c2c;
}

.ui.secondary.button:active {
background: #dbdbdb;
color: #2c2c2c;
}

Connecting Designer

4.10. Finally, run the project. Click on the right-mouse-button in the created application and go to preferred browser.

Run Project

Afterward, the Designer will be available.

Designer

More information related to how to build and connect Workflow Engine Designer can be read here. You can see also:

In case of any issue, check for errors on the page and refer to FAQ. You can learn more about Designer in documentation.

caution

If you see some strange designer appearance, for example a modal windows or check boxes are looking strange, it probably means a 'css' conflict. Feel free to send us a message thought the contact form or at support@optimajet.com, we will do our best to help you.

5. Creating a workflow scheme

5.1. Create two commands: 'go' and 'back'.

Commands

5.2. Create four activities: 'Start', 'State1', 'State2' and 'End'. Set the Initial flag for the Start activity and the Final flag for the End activity.

Activities

5.3. Create transitions between the activities. For each transition set the Trigger (in this example is set Command as trigger type) and Classifier parameter (it can be: Not Specified, Direct or Reverse).

Transitions

5.4. Click on 'Save Scheme' button.

You can learn more about Schemes in documentation.

FinalScheme

The final scheme can be downloaded here.

6. Creating a process and calling commands

Remember

Starting with WFE 7.0, the artifacts for the .NET Framework are no longer available. The .NET Framework 4.6.2 is now the minimum supported version (netstandard2.0), except for Oracle Provider, for which the minimum version is .NET Core 3.0 (netstandard2.1).

6.1. Create a Console Application project.

6.2. Add the following NuGet packages to the project (.NET Framework / .NET Core):

6.3. Create and add a Reference to the project: WorkflowLib from step 3.

6.4. Add a database connection string.

<connectionStrings>
<add name="ConnectionString"
connectionString="Data Source=(local);Initial Catalog=WFE;Integrated Security=False;User ID=sa;Password=1"
providerName="System.Data.SqlClient" />
</connectionStrings>
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=(local);Initial Catalog=wfe_sample;User ID=sa;Password=1"
}
}
Be aware

Make sure that server address, authentication type, login and password match your settings.

6.5. Read documentation in the section how to manage workflows.

6.6. Add the following namespaces to the Program.cs file:

info

The following lines of code can be different and usually depends on the class library name created in step 2. For example, you have to write "using ClassLibrary;" instead of "using WorkflowLib;" if you defined the library as ClassLibrary.

using OptimaJet.Workflow.Core.Runtime;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using WorkflowLib;

Add the following usings for .NET Core:

using System.IO;
using Microsoft.Extensions.Configuration;

Here's a sample code that shows you how to call basic operations: creating a process, getting a list of available commands, executing commands, setting a state and deleting a process:

class Program
{
static string schemeCode = "SimpleWF";
static Guid? processId = null;
static void Main(string[] args)
{
// ------------------------------------------------------
// TODO: This code for .NET Core only (!).
// Comment it if you're working with .NET Framework
// ------------------------------------------------------
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);

IConfigurationRoot configuration = builder.Build();
WorkflowInit.ConnectionString = configuration[$"ConnectionStrings:DefaultConnection"];
// ------------------------------------------------------

Console.WriteLine("Operation:");
Console.WriteLine("0 - CreateInstance");
Console.WriteLine("1 - GetAvailableCommands");
Console.WriteLine("2 - ExecuteCommand");
Console.WriteLine("3 - GetAvailableState");
Console.WriteLine("4 - SetState");
Console.WriteLine("5 - DeleteProcess");
Console.WriteLine("9 - Exit");
Console.WriteLine("The process isn't created.");
CreateInstance();

do
{
if (processId.HasValue)
{
Console.WriteLine("ProcessId = '{0}'. CurrentState: {1}, CurrentActivity: {2}",
processId,
WorkflowInit.Runtime.GetCurrentStateName(processId.Value),
WorkflowInit.Runtime.GetCurrentActivityName(processId.Value));
}

Console.Write("Enter code of operation:");
char operation = Console.ReadLine().FirstOrDefault();

switch (operation)
{
case '0':
CreateInstance();
break;
case '1':
GetAvailableCommands();
break;
case '2':
ExecuteCommand();
break;
case '3':
GetAvailableState();
break;
case '4':
SetState();
break;
case '5':
DeleteProcess();
break;
case '9':
return;
default:
Console.WriteLine("Unknown code. Please, repeat.");
break;
}
Console.WriteLine();
} while (true);
}

private static void CreateInstance()
{
processId = Guid.NewGuid();
try
{
WorkflowInit.Runtime.CreateInstance(schemeCode, processId.Value);
Console.WriteLine("CreateInstance - OK.", processId);
}
catch (Exception ex)
{
Console.WriteLine("CreateInstance - Exception: {0}", ex.Message);
processId = null;
}
}

private static void GetAvailableCommands()
{
if (processId == null)
{
Console.WriteLine("The process isn't created. Please, create process instance.");
return;
}
var commands = WorkflowInit.Runtime.GetAvailableCommands(processId.Value, string.Empty);

Console.WriteLine("Available commands:");
if (commands.Count() == 0)
{
Console.WriteLine("Not found!");
}
else
{
foreach (var command in commands)
{
Console.WriteLine("- {0} (LocalizedName:{1}, Classifier:{2})",
command.CommandName, command.LocalizedName, command.Classifier);
}
}
}

private static void ExecuteCommand()
{
if (processId == null)
{
Console.WriteLine("The process isn't created. Please, create process instance.");
return;
}
WorkflowCommand command = null;
do
{
GetAvailableCommands();
Console.Write("Enter command:");
var commandName = Console.ReadLine().ToLower().Trim();
if (commandName == string.Empty)
return;
command = WorkflowInit.Runtime
.GetAvailableCommands(processId.Value, string.Empty)
.Where(c => c.CommandName.Trim().ToLower() == commandName).FirstOrDefault();
if (command == null)
Console.WriteLine("The command isn't found.");
} while (command == null);

WorkflowInit.Runtime.ExecuteCommand(command, string.Empty, string.Empty);
Console.WriteLine("ExecuteCommand - OK.", processId);
}

private static void GetAvailableState()
{
if (processId == null)
{
Console.WriteLine("The process isn't created. Please, create process instance.");
return;
}
var states = WorkflowInit.Runtime
.GetAvailableStateToSet(processId.Value, Thread.CurrentThread.CurrentCulture);
Console.WriteLine("Available state to set:");
if (states.Count() == 0)
{
Console.WriteLine("Not found!");
}
else
{
foreach (var state in states)
{
Console.WriteLine("- {0}", state.Name);
}
}
}

private static void SetState()
{
if (processId == null)
{
Console.WriteLine("The process isn't created. Please, create process instance.");
return;
}
string stateName = string.Empty;
WorkflowState state;
do
{
GetAvailableState();
Console.Write("Enter state:");
stateName = Console.ReadLine().ToLower().Trim();
if (stateName == string.Empty)
return;
state = WorkflowInit.Runtime
.GetAvailableStateToSet(processId.Value, Thread.CurrentThread.CurrentCulture)
.Where(c => c.Name.Trim().ToLower() == stateName).FirstOrDefault();
if (state == null)
Console.WriteLine("The state isn't found.");
else
break;
} while (true);
if (state != null)
{
WorkflowInit.Runtime.SetState(processId.Value, string.Empty, string.Empty,
state.Name, new Dictionary<string, object>());
Console.WriteLine("SetState - OK.", processId);
}
}

private static void DeleteProcess()
{
if (processId == null)
{
Console.WriteLine("The process isn't created. Please, create process instance.");
return;
}
WorkflowInit.Runtime.DeleteInstance(processId.Value);
Console.WriteLine("DeleteProcess - OK.", processId);
processId = null;
}
}

6.7. Click on right-mouse-button to Run or Debug the Console Application project.

Run Project

Then, the Console Application where you can create processes and execute commands should start.

Console Application

info

Learn more about basic operations here.

Conclusion

Workflow Engine is one of the easiest workflow engines for document approval when integrating is required. We recommend it to companies that develop information systems with workflow functionality.

In addition, you can download our samples here.

If you have any question, please, do not hesitate to contact us.