States and Activities
Tutorial 5 📫
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.
The following stages are covered:
SetState
,SetActivity
andResume
methods realization.- Change process state functionality operation.
More information regarding Activities
and States
can be read in this section
Prerequisites​
- You should go through previous tutorials OR clone: Action interaction with SignalR branch.
- JetBrains Rider or Visual Studio.
- 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
.
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.
Download the updated ParametersScheme.xml
here.
Click on tab Designer
to upload the provided scheme as indicated below:
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.
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
.
Overall, the new Activities: Calculating priority
, CommonProcessing
and HighPriorityProcessing
, were added. Now, let's see
Calculating priority
Activity where two new CodeActions are included.
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
.
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.
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
.
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.
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
.
The commands from CommonProcessing
can be performed by the supporter as it was indicated in previous tutorials.
Likewise, the message: 'Issue is high priority processing', and the same parameters are added in the state HighPriorityProcessing
.
In this case the commands are available not only for the supporter but also for the manager.
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.
In this case, the priority is calculated, so the process moves on to HighPriorityProcessing
.
Moreover, the state changes in the workflow will be visualized in the 'History' tab when you check it within 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'.
However, the Description parameter can not be modified when redirecting by 'Command parameters'.
Once redirect
command is executed, the request is redirected to IT Department, and it is kept it in the state HighPriorityProcessing
.
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'.
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.
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.
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.
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.
Once, the process instance is running, click on 'Change process state' to edit the process parameters.
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'.
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
.
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'.
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.
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.