Skip to main content

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

States and Activities

Tutorial 5 📫

Source code

actions-signalr - initial branch
states-and-activities - final branch
states-and-activities pull request - pull request with code changes

Overview​

In this tutorial the main differences between States and Activities are described and the following methods: SetState, SetActivity and Resume are implemented on the server and native client.

States and Activities

The following stages are covered:

  1. SetState, SetActivity and Resume methods realization.
  2. Change process state functionality operation.
take into account

More information regarding Activities and States can be read in this section

Prerequisites​

  1. You should go through previous tutorials OR clone: Action interaction with SignalR branch.
  2. JetBrains Rider or Visual Studio.
  3. Command prompt or Terminal.

Backend​

The code improvements that are required in the backend are described in this section.

Users​

First, the class Users was updated. The support process that was introduced previously is complemented, and new users are added in the divisions, so will have two employees in each department at least, one with both roles: 'User' and 'Manager', and another one with 'User' role.

public static readonly List<User> Data = new()
{
new User {Name = "Peter", Roles = new List<string> {"User", "Manager"}, Division = "IT Department"},
new User {Name = "Paula", Roles = new List<string> {"User"}, Division = "IT Department"},
new User {Name = "Margaret", Roles = new List<string> {"User"}, Division = "First Line"},
new User {Name = "Steven", Roles = new List<string> {"User", "Manager"}, Division = "First Line"},
new User {Name = "John", Roles = new List<string> {"Manager"}, Division = "Accounting"},
new User {Name = "Emily", Roles = new List<string> {"User", "Manager"}, Division = "Accounting"},
new User {Name = "Sam", Roles = new List<string> {"User"}, Division = "Law Department"},
new User {Name = "Samantha", Roles = new List<string> {"User", "Manager"}, Division = "Law Department"},
};

public static readonly Dictionary<string, User> UserDict = Data.ToDictionary(u => u.Name);

ProcessActivityDto​

Next, a new class ProcessActivityDto is included in the data models. Here, we have the attributes Name, IsCurrent which indicates the current Activity where the process is, and if there is a state, then it is sent through State.

namespace WorkflowApi.Models;

public class ProcessActivityDto
{
public string Name { get; set; }

public bool IsCurrent { get; set; }

public ProcessStateDto? State { get; set; }
}

ProcessStateDto​

Moreover, the class ProcessStateDto is added. Here, we have Name, IsCurrent, but also LocalizedName and the helper method FromProcessState, which creates the DTO model from Workflow Engine object.

using OptimaJet.Workflow.Core.Runtime;

namespace WorkflowApi.Models;
public class ProcessStateDto
{
public string Name { get; set; }

public string LocalizedName { get; set; }

public bool IsCurrent { get; set; }

public static ProcessStateDto? FromProcessState(WorkflowState? state, string currentState)
{
if (state == null)
{
return null;
}

return new ProcessStateDto
{
Name = state.Name,
LocalizedName = state.VisibleName,
IsCurrent = state.Name == currentState
};
}
}

WorkflowController​

Furthermore, the WorkflowController.cs is modified. There are several new methods that have been included.

The first method GetProcessStates, it returns all states of the process which the State can be set.

/// <summary>
/// Returns list of process states that can be set
/// </summary>
/// <param name="processId">Unique process identifier</param>
/// <returns>List of process states</returns>
[HttpGet]
[Route("states/{processId:guid}")]
public async Task<IActionResult> GetProcessStates(Guid processId)
{
var processStates = await WorkflowInit.Runtime.GetAvailableStateToSetAsync(processId);

var processInstance = await GetProcessInstanceAsync(processId);

if (processInstance == null)
{
return NotFound();
}
var processStateDtos = processStates.Select(state => ProcessStateDto.FromProcessState(state, processInstance.CurrentState)).ToList();

return Ok(processStateDtos);
}

Second, we have GetProcessActivities method that returns all activities of the process.

/// <summary>
/// Returns list of process activities
/// </summary>
/// <param name="processId">Unique process identifier</param>
/// <returns>List of process activities</returns>
[HttpGet]
[Route("activities/{processId:guid}")]
public async Task<IActionResult> GetProcessActivities(Guid processId)
{
var processInstance = await GetProcessInstanceAsync(processId);

if (processInstance == null)
{
return NotFound();
}

var processStates = await WorkflowInit.Runtime.GetAvailableStateToSetAsync(processId);

var processActivityDtos = processInstance.ProcessScheme.Activities.Select(a => new ProcessActivityDto
{
Name = a.Name,
State = ProcessStateDto.FromProcessState(processStates.FirstOrDefault(s => s.Name == a.State),
processInstance.CurrentState),
IsCurrent = a.Name == processInstance.CurrentActivityName
});

return Ok(processActivityDtos);
}

The third method SetState where the ProcessParametersDto, State name, processId can be passed. In this method is generated the object SetStateParams which is a Workflow Engine object, next the SetStateAsync is invoked, and in stateWasChanged is checked if state was changed. The helper method GetProcessInstanceAsync is called, and then validated that CurrentState, it does not equal to process state before executing SetState.

/// <summary>
/// Sets process state
/// </summary>
/// <param name="processId">Unique process identifier</param>
/// <param name="state">State name</param>
/// <param name="identityId">Set state executor identifier</param>
/// <param name="dto">New process parameters</param>
/// <returns>true if state was changed</returns>
[HttpPost]
[Route("setState/{processId:guid}/{state}/{identityId}")]
public async Task<IActionResult> SetState(Guid processId, string state, string identityId,
[FromBody] ProcessParametersDto dto)
{
var setStateParams = new SetStateParams(processId, state)
{
IdentityId = identityId
};

if (dto.ProcessParameters.Count > 0)
{
var processScheme = await WorkflowInit.Runtime.GetProcessSchemeAsync(processId);

foreach (var processParameter in dto.ProcessParameters)
{
var (name, value) =
GetParameterNameAndValue(processParameter.Name, processParameter.Value, processScheme);

if (processParameter.Persist)
{
setStateParams.AddPersistentParameter(name, value);
}
else
{
setStateParams.AddTemporaryParameter(name, value);
}
}
}

var previousState = (await GetProcessInstanceAsync(processId))?.CurrentState;

await WorkflowInit.Runtime.SetStateAsync(setStateParams);

var stateWasChanged = (await GetProcessInstanceAsync(processId))?.CurrentState !=
previousState;

return Ok(stateWasChanged);
}

Then, we have SetActivity method. It is intended for non-standard cases, so the code is more complex. Here, the methods SetActivityWithExecutionAsync and GetProcessInstanceAsync are invoked. The method SetActivityWithoutExecutionAsync is used rarely, so it is not considered in this example.

/// <summary>
/// Sets process activity
/// </summary>
/// <param name="processId">Unique process identifier</param>
/// <param name="activity">Activity name</param>
/// <param name="identityId">Set activity executor identifier</param>
/// <param name="dto">New process parameters</param>
/// <returns>true if activity was changed</returns>
[HttpPost]
[Route("setActivity/{processId:guid}/{activity}/{identityId}")]
public async Task<IActionResult> SetActivity(Guid processId, string activity, string identityId,
[FromBody] ProcessParametersDto dto)
{
var processInstance = await WorkflowInit.Runtime.GetProcessInstanceAndFillProcessParametersAsync(processId);

var activityToSet = processInstance.ProcessScheme.Activities.FirstOrDefault(a => a.Name == activity);

if (activityToSet == null)
{
return Ok(false);
}

var newParameters = new Dictionary<string, object>();

foreach (var processParameter in dto.ProcessParameters)
{
var (name, value) =
GetParameterNameAndValue(processParameter.Name, processParameter.Value,
processInstance.ProcessScheme);

newParameters.Add(name,value);
}

var previousActivity = processInstance.CurrentActivity.Name;

await WorkflowInit.Runtime.SetActivityWithExecutionAsync(identityId, identityId,
newParameters, activityToSet, processInstance);

var activityWasChanged =
(await GetProcessInstanceAsync(processId))?.CurrentActivity.Name !=
previousActivity;

return Ok(activityWasChanged);
}

In addition, the method Resume. It is required for recovery in case of failures. The ResumeParams is created, the processParameter is set, the method ResumeAsync is called and resumeResult.WasResumed is returned.

We will clarify how Resume feature works in the section Running the process

/// <summary>
/// Resumes process state
/// </summary>
/// <param name="processId">Unique process identifier</param>
/// <param name="activity">Activity name</param>
/// <param name="identityId">Resume executor identifier</param>
/// <param name="dto">New process parameters</param>
/// <returns>true if process was resumed</returns>
[HttpPost]
[Route("resume/{processId:guid}/{activity}/{identityId}")]
public async Task<IActionResult> Resume(Guid processId, string activity, string identityId,
[FromBody] ProcessParametersDto dto)
{
var resumeParams = new ResumeParams(processId, activity)
{
IdentityId = identityId
};

if (dto.ProcessParameters.Count > 0)
{
var processScheme = await WorkflowInit.Runtime.GetProcessSchemeAsync(processId);

foreach (var processParameter in dto.ProcessParameters)
{
var (name, value) =
GetParameterNameAndValue(processParameter.Name, processParameter.Value, processScheme);

if (processParameter.Persist)
{
resumeParams.AddPersistentParameter(name, value);
}
else
{
resumeParams.AddTemporaryParameter(name, value);
}
}
}

var resumeResult = await WorkflowInit.Runtime.ResumeAsync(resumeParams);

return Ok(resumeResult.WasResumed);
}

Finally, the private method GetProcessInstanceAsync was added. It provides a faster way to get GetProcessInstanceAsync just without loading the process parameters that are kept in the database.

When the standard method Runtime is called, it uses the method GetProcessIntanceFillProcessParameters for uploading the process instance, process parameters and so on, but most of these parameters are not required in this case.

We need system parameters mostly, so we build processInstance and call the method FillSystemProcessParametersAsync.

system parameters

More information regarding system parameters can be read in this section.

private static async Task<ProcessInstance?> GetProcessInstanceAsync(Guid processId)
{
//it will be faster to use WorkflowInit.Runtime.Builder.GetProcessInstanceAsync call,
//because it doesn't load process parameters
var processInstance = await WorkflowInit.Runtime.Builder.GetProcessInstanceAsync(processId);

if (processInstance == null)
{
return null;
}

await WorkflowInit.Runtime.PersistenceProvider.FillSystemProcessParametersAsync(processInstance);
return processInstance;
}

Frontend​

The code improvements required in the frontend are described in this section.

Lodash installation​

First, the Lodash library must be installed in frontend directory, so in terminal window execute the following command:

npm install lodash

ProcessMenuChangeState​

The new ProcessMenuChangeState React component is added. It gives access to the operations: SetState, SetActivity and Resume at once. In this component are loaded the activities, states, process parameters, and also a new modal window 'Select your action' is created. The component enables to interact with the methods: SetState, SetActivity, Resume, and depending on the selected action, different operations are displayed. Besides, the process parameters are available for editing, and they can be passed along with the methods.

import {useEffect, useState} from "react";
import {Button, InputPicker, Message, Modal, useToaster} from "rsuite";
import ProcessParameters from "./ProcessParameters";
import settings from "./settings";
import {startCase} from 'lodash'

const actions = ['SetState', 'SetActivity', 'Resume'].map(
item => ({label: startCase(item), value: item})
);

const ProcessMenuChangeState = ({open, onClose, processId, currentUser}) => {

const [selectedAction, setSelectedAction] = useState('SetState')
const [newProcessParameters, setNewProcessParameters] = useState([])
const initialData = {
states: [],
currentState: '',
activities: [],
currentActivity: '',
processParameters: [],
loaded: false
};
const [data, setData] = useState(initialData)
const toaster = useToaster();

const loadData = (processId) => {
const fetches = []
const data = {}
const statesRequest = fetch(`${settings.workflowUrl}/states/${processId}/`)
.then(result => result.json())
.then(result => {
Object.assign(data, {
states: result.map(item => ({label: item.localizedName, value: item.name})),
currentState: result.find(item => !!item.isCurrent)?.name ?? result[0]?.name
})
return result
})
fetches.push(statesRequest)
const activitiesRequest = fetch(`${settings.workflowUrl}/activities/${processId}/`)
.then(result => result.json())
.then(result => {
Object.assign(data, {
activities: result.map(item => ({label: item.name, value: item.name})),
currentActivity: result.find(item => !!item.isCurrent)?.name
})
return result
})
fetches.push(activitiesRequest)
const parametersRequest = fetch(`${settings.workflowUrl}/schemeParameters/${processId}/`)
.then(result => result.json())
.then(result => {
Object.assign(data, {
...data,
processParameters: result
})
return result
})
fetches.push(parametersRequest)

Promise.all(fetches).then(_ => {
Object.assign(data, {
loaded: true
});
setData(data)
})
}

useEffect(() => {
if (open) {
loadData(processId);
} else {
setData(initialData)
}
}, [processId, open]);

const execute = () => {
fetch(`${settings.workflowUrl}/${selectedAction}/${processId}/${getSelectedStateOrActivity()}/${currentUser}/`,
{
method: "post",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
processParameters: newProcessParameters
})
})
.then(result => {
if (result.status !== 200) {
throw new Error(result.statusText)
}
return result.json()
})
.then(() => {
onClose(true)
})
.catch((error) => {
const message = <Message type="error" closable={true}> {error.toString()} </Message>
toaster.push(message, {placement: 'topCenter', duration: 20000})
onClose(true)
})
}

const getSelectedStateOrActivity = () => {
return selectedAction === 'SetState' ? data.currentState : data.currentActivity;
}

return <Modal open={open && data.loaded} onClose={onClose} overflow={true}>
<Modal.Header>
<Modal.Title>Select your action</Modal.Title>
</Modal.Header>
<Modal.Body>
<InputPicker data={actions} value={selectedAction} onChange={(value) => setSelectedAction(value)}/>
{selectedAction === 'SetState' &&
<InputPicker data={data.states} value={data.currentState}
onChange={(value) => setData({...data, currentState: value})}/>
}
{selectedAction !== 'SetState' &&
<InputPicker data={data.activities} value={data.currentActivity}
onChange={(value) => {
setData({...data, currentActivity: value})
}}/>
}
<ProcessParameters onParametersChanged={(processParameters) => setNewProcessParameters(processParameters)}
defaultParameters={data.processParameters}/>
</Modal.Body>
<Modal.Footer>
<Button onClick={execute} appearance="primary">
{`${startCase(selectedAction)} ${selectedAction === 'Resume' ? 'from' : ''} ${getSelectedStateOrActivity()}`}
</Button>
<Button onClick={() => onClose(false)} appearance="subtle">
Cancel
</Button>
</Modal.Footer>
</Modal>
}

export default ProcessMenuChangeState;

ProcessMenu​

The ProcessMenu component is modified. The new modal element was included.

import React, {useEffect, useState} from "react";
import {Button, ButtonGroup, FlexboxGrid, Modal} from "rsuite";
import FlexboxGridItem from "rsuite/cjs/FlexboxGrid/FlexboxGridItem";
import settings from "./settings";
import Users from "./Users";
import ProcessParameters from "./ProcessParameters";
import ProcessMenuChangeState from "./ProcessMenuChangeState";

const ProcessMenu = (props) => {
const [commands, setCommands] = useState([]);
const [currentUser, setCurrentUser] = useState();

...
const loadCommands = (processId, user) => {
fetch(`${settings.workflowUrl}/commands/${processId}/${user}`)
.then(result => result.json())
.then(result => {
setCommands(result.commands)
})
}

const [openChangeState, setOpenChangeState] = useState(false)

...

const buttons = commands.map(c => <Button key={c.name} onClick={() => onOpenCommandWindow(c)}>{c.localizedName}</Button>)
return <>
<FlexboxGrid>
<FlexboxGridItem colspan={4}>
<Users onChangeUser={setCurrentUser} currentUser={currentUser}/>
</FlexboxGridItem>
<FlexboxGridItem colspan={12}>
{commands.length > 0 && <ButtonGroup>
<Button disabled={true}>Commands:</Button>
{buttons}
</ButtonGroup>
}
<Button onClick={onOpenProcessParametersWindow}>Change process parameters.</Button>
<Button onClick={() => setOpenChangeState(true)}>Change process state.</Button>
</FlexboxGridItem>
</FlexboxGrid>

...

<Modal open={processParametersState.open} onClose={onCloseProcessParametersWindow} overflow={true}>
<Modal.Header>
<Modal.Title>Change process parameters</Modal.Title>
</Modal.Header>
<Modal.Body>
<ProcessParameters onParametersChanged={(processParameters) => setNewProcessParameters(processParameters)}
defaultParameters={processParametersState.processParameters}/>
</Modal.Body>
<Modal.Footer>
<Button onClick={onSetProcessParameters} appearance="primary">
Save
</Button>
<Button onClick={onCloseProcessParametersWindow} appearance="subtle">
Cancel
</Button>
</Modal.Footer>
</Modal>
<ProcessMenuChangeState open={openChangeState} onClose={(executed) => {
setOpenChangeState(false)
if (executed) {
props.afterCommandExecuted?.();
}
}} processId={props.processId} currentUser={currentUser}/>
</>
}
export default ProcessMenu;

Starting the application​

Once completing the code improvements, the application can be run, so start it by executing the following commands:

cd docker-files
docker compose up --build --force-recreate

Then, open the application URL http://localhost:3000/ in your preferred browser.

Upload the process scheme​

In previous tutorials the ParametersScheme.xml was presented to demonstrate how to work with process parameters, but in this case several changes were included in the scheme. These new adjustments are explained in the next section.

Process Scheme

Download the updated ParametersScheme.xml here.

Click on tab Designer to upload the provided scheme as indicated below:

Upload scheme

Then, save the scheme.

Running the process​

Process triggering​

The Support process described in the tutorial Introducing parameters was modified. Now, a priority condition is verified when a request is received or redirected. If the ticket is High priority, then it is sent to the manager who must check the issue thoroughly.

If the Activity State is filled and the checkbox For set state is set, then it will be the entry point for the specified State.

keep in mind

The checkmark For set state can be enabled, and the field State is available in the Activities.

First, whe have the Activity Processing as in previous tutorials, but in this case, the message 'Issue is analyzing.' is displayed in the process console by setting the Action SendMessageToProcessConsoleAsync.

Processing

Overall, the new Activities: Calculating priority, CommonProcessing and HighPriorityProcessing, were added. Now, let's see Calculating priority Activity where two new CodeActions are included.

Calculating priority

A simple CodeAction Calculate prioriry is called in the Activity Calculating priority. It takes the request Description and checks the 'urgent', 'priority', 'emergency' or 'high' mentions. If these references are indicated in the Description, then a temporary process parameter HighPriority is designated as true, otherwise it is set as false.

CodeAction 1

In addition, another CodeAction Throw Exception is included. It is required to check how Resume method works, and it is controlled from outside the process via ThrowError parameter.

CodeAction 2

The field State is not set in the Activity Calculating priority. It means that, the process state is not changed when this activity is being completed. The state Processing is set in the Activity CommonProcessing which redirects the process through Calculating priority. Similarly, the state HighPriorityProcessing is set in the Activity HighPriorityProcessing, and also the process is redirected by Calculating priority.

The activity Calculating priority is considered part of CommonProcessing and HighPriorityProcessing at the same time. The process state is kept as Processing state or HighPriorityProcessing state while the issue is not updated or resolved.

Moreover, we have a simplified Activity, the Decision High Priority?, where the @HighPriority parameter is checked. If it is a present parameter, and it is equal to true, the process goes to HighPriorityProcessing Activity, otherwise it moves on the CommonProcessing.

Decision

From CommonProcessing activity all commands: redirect, resolve and reject are handled by the supporter rule, as it was explained in previous tutorials. In case of HighPriorityProcessing, these commands are available for the manager CheckRole rule also.

Commands and actors

When a ticket Description is modified or updated, and its priority changes, for instance, it is set as high priority ticket, then the request will be redirected through HighPriorityProcessing Activity. Otherwise, it will be handled by CommonProcessing until its fulfillment.

The message: 'Issue is processing' is inserted in the CommonProcessing state, and the process parameters: @Division, issue @Description and the @Comment.

Common processing

The commands from CommonProcessing can be performed by the supporter as it was indicated in previous tutorials.

Command redirect

Likewise, the message: 'Issue is high priority processing', and the same parameters are added in the state HighPriorityProcessing.

High priority processing

In this case the commands are available not only for the supporter but also for the manager.

Command resolve

The Resolved and Rejected Activities are kept without changes.

Process execution​

We describe the support process execution in this section.

Initially, we have the high priority tickets handling. You should click on button Create process and fill out the issue Description, the Division, and the Comment in the modal window 'Initial process parameters'. Then, click on blue button 'Create process'. In the Description must be indicated that the issue is an urgent request.

Create process

In this case, the priority is calculated, so the process moves on to HighPriorityProcessing.

High priority instance

Moreover, the state changes in the workflow will be visualized in the 'History' tab when you check it within Process info.

Process info

Next, if you select a user with Manager role from First Line (L1), the commands become available. Here, you can click on command redirect in modal window 'Command parameters'.

Command redirect

However, the Description parameter can not be modified when redirecting by 'Command parameters'.

Command parameters

Once redirect command is executed, the request is redirected to IT Department, and it is kept it in the state HighPriorityProcessing.

Once redirecting

Change process state​

Now, let's see 'Change process state' functionality in detail.

Set State​

Indeed, we can check the usual support process, so we are going to update the process state through Set State feature in modal window 'Select your action'. Here, only three states: Processing, Resolved and Rejected can be chosen. Select the state Processing, and write the new Description. After that, click on blue button 'Set State processing'.

Set State Processing

Remember

The process state can not be set in HighPriorityProcessing and CommonProcessing through Set State in modal window 'Change process state', so in this case the checkmark 'For Set State' is not enabled in these activities. Nevertheless, the process states might be restricted by using 'For Set State' if it is required.

In this case, the process goes on to CommonProcessing automatically, and its new state 'Issue is processing' is updated and displayed in the Process Console. The checkmark 'For Set State' is enabled in the input activity Processing, in this way the process can be run from this activity, and its actions executed.

Common Processing instance

Set Activity​

Furthermore, we have Set Activity functionality which might be used in extraordinary cases. In the modal window 'Select your Action' you can choose any Activity, and the parameters might be edited or passed along with it. Previously, the process went to CommonProcessing, so now it can be sent back to HighPriorityProcessing. You must update the Description field by writing 'urgent issue'. The parameters can be edited if it is required.

Set Activity

Then, the process state is skipped from CommonProcessing to a different state through Calculating priority activity where actions are fulfilled and, as result, the support request moves on to HighPriorityProcessing. The Processing activity is not executed again and the message Issue is analyzing is not displayed by process console respectively.

Request updated to high priority

don't forget

Even though Set Activity feature is available, we recommend using Set State or Resume in most cases.

Resume​

Finally, the Resume feature. You should create a new process and indicate that the issue requires urgent support.

Create new process

Once, the process instance is running, click on 'Change process state' to edit the process parameters.

Process instance running

Include a new parameter called ThrowError and set it as true in the modal window 'Select your action'. Choose Set State action and the Processing state. Then, click on button 'Set state processing'.

Error parameter

Here, the activity Calculating priority is not completed due the exception error which takes place. The actions defined in Processing are performed and the message 'Issue is analyzed' is displayed by Process Console.

Exception error

In this case we can invoke the method Resume to reset the process from Processing activity without executing once again the implemented actions. Therefore, go to 'Change process state'. Now, you can select the Resume action and State Processing for resetting the process. Besides, ThrowError parameter must be set as false. After that, click on button 'Resume from Processing'.

Error parameter update

note

Setting the current state of the process, it is highly recommended, but also you might select different states for resuming. The parameters LastError and LastErrorStackTrace show up automatically when a failure occurs.

Afterward, the process will move on to HighPriorityProcessing activity as we expected, and the implemented actions in Processing activity won't be executed once more.

Process resume

what's the main difference?

When Set State is enabled in Processing state, the Processing activity is executed and the process goes ahead. If for some reason the process depends on Processing state and Resume is set in this state, then the specified Actions in Processing state won't be fulfilled, but the process will move forward.

Conclusion​

In this tutorial we demonstrated some useful operations that provides Workflow Engine and implemented the methods: SetState, SetActivity and Resume.

In general, Set State and Resume are the most important operations, Set State for instance, can be used for skipping the process from one state to specific one by the system admin. It is set by enabling the checkmark 'For set state' and filling out the field called 'State' in the Activity window. Resume is used for recovering after failures mainly, and Set Activity that is not recommended, but it may be used in case of statical analysis or special cases only.