Appearance
Workflow Service
The Workflow service provides a business process automation engine for Microtec ERP. It allows non-developers to define approval workflows, routing rules, and automated actions through the Blazor Workflow Designer, then executes those workflows at runtime via Azure Service Bus events.
Overview
| Property | Value |
|---|---|
| Service | Workflows.Apis |
| CAE placement | Private CAE |
| Gateway route prefix | /workflow-apis/ |
| Source | Platforms/Src/InfrastructureServices/Workflow/ |
| Designer | WorkflowDesigner/ (Blazor Server) |
| Event bus | Azure Service Bus |
| Auth realm | microtec (Keycloak) |
Architecture
Workflow Designer
The WorkflowDesigner is a Blazor Server application that provides a drag-and-drop canvas for designing workflows. It is a separate deployable app, not part of the backend API.
WorkflowDesigner/
├── WorkflowDesigner.App/ # Blazor Server entry point
├── WorkflowDesigner.Components/ # Drag-and-drop canvas, node palette
├── WorkflowDesigner.Domain/ # Workflow graph models
└── WorkflowDesigner.Infrastructure/ # Persist to Workflows.ApisWorkflow definitions are saved as JSON graphs and stored via the Workflows.Apis REST endpoint. The designer is accessed by platform admins and is not exposed to end users.
Access
The Workflow Designer is typically deployed at an internal URL (e.g., https://workflow-designer.<env>.azurecontainerapps.io) and is not routed through the main Gateway.
Domain Model
Step Types
| Step Type | Enum | Description |
|---|---|---|
| Approval | StepType.Approval | Requires a user to approve or reject |
| Notification | StepType.Notification | Sends a message via Notification service |
| Condition | StepType.Condition | Evaluates a rule and branches |
| AutoAction | StepType.AutoAction | Executes a callback to the originating service |
| Parallel | StepType.Parallel | Runs multiple steps simultaneously |
| Delay | StepType.Delay | Waits for a defined duration |
Runtime Execution Flow
API Endpoints
Workflow Definitions
| Method | Route | Operation |
|---|---|---|
GET | /WorkflowDefinitions | List definitions |
GET | /WorkflowDefinitions/{Id} | Get by ID including JSON graph |
POST | /WorkflowDefinitions | Save new definition |
PUT | /WorkflowDefinitions | Update definition (creates new version) |
POST | /WorkflowDefinitions/{Id}/Activate | Activate a version |
Workflow Instances
| Method | Route | Operation |
|---|---|---|
GET | /WorkflowInstances | List instances (filter by entity, status) |
GET | /WorkflowInstances/{Id} | Get instance with steps |
POST | /WorkflowInstances/Trigger | Start a new workflow instance |
POST | /WorkflowInstances/{Id}/Approve | Approve current step |
POST | /WorkflowInstances/{Id}/Reject | Reject current step |
POST | /WorkflowInstances/{Id}/Cancel | Cancel running instance |
My Approvals
| Method | Route | Operation |
|---|---|---|
GET | /MyApprovals | Pending approvals for current user |
GET | /MyApprovals/History | Completed approvals by current user |
IWorkflowPublicApi
Consuming services trigger workflows using this typed client:
csharp
// Defined in Microtec.PublicApi.AppsPortal
public interface IWorkflowPublicApi
{
/// <summary>
/// Start a workflow instance for a given entity.
/// Returns the workflow instance ID for tracking.
/// </summary>
Task<int> TriggerAsync(
TriggerWorkflowRequest request,
CancellationToken ct = default);
/// <summary>
/// Check the current status of a workflow instance.
/// </summary>
Task<WorkflowStatusDto> GetStatusAsync(
int instanceId,
CancellationToken ct = default);
/// <summary>
/// Cancel a running workflow instance.
/// </summary>
Task CancelAsync(int instanceId, CancellationToken ct = default);
}TriggerWorkflowRequest
csharp
public record TriggerWorkflowRequest(
int TenantId,
string EntityType,
int EntityId,
string WorkflowCode,
int InitiatedByUserId,
Dictionary<string, string>? Metadata = null,
string? CallbackUrl = null);Usage Example
Triggering a Purchase Order Approval Workflow
csharp
// Inside AddPurchaseOrderCommandHandler
public async Task<ApiResponse<int>> Handle(AddPurchaseOrderCommand request, CancellationToken ct)
{
var po = PurchaseOrder.Create(request.Dto.SupplierId, request.Dto.Lines);
await unitOfWork.Repository<PurchaseOrder>().AddAsync(po, ct);
await unitOfWork.SaveChangesAsync(ct);
// Only trigger workflow if PO exceeds approval threshold
if (po.Total > 10_000m)
{
var instanceId = await workflowApi.TriggerAsync(new TriggerWorkflowRequest(
TenantId: tenantProvider.TenantId,
EntityType: "PurchaseOrder",
EntityId: po.Id,
WorkflowCode: "PO_APPROVAL",
InitiatedByUserId: currentUser.UserId,
Metadata: new()
{
["Total"] = po.Total.ToString("N2"),
["SupplierName"] = po.SupplierName
},
CallbackUrl: $"{serviceBaseUrl}/PurchaseOrders/WorkflowCallback"
), ct);
po.SetWorkflowInstance(instanceId);
await unitOfWork.SaveChangesAsync(ct);
}
return ApiResponse.Success(po.Id);
}Handling the Approval Callback
csharp
// PurchaseOrdersController.cs
[HttpPost("WorkflowCallback")]
public async Task<IActionResult> WorkflowCallback([FromBody] WorkflowCallbackDto dto)
=> await mediator.Send(new HandlePurchaseOrderWorkflowCallbackCommand(dto)).ToActionResult();
// HandlePurchaseOrderWorkflowCallbackCommandHandler.cs
public async Task<ApiResponse> Handle(HandlePurchaseOrderWorkflowCallbackCommand request, CancellationToken ct)
{
var po = await unitOfWork.Repository<PurchaseOrder>()
.GetByIdAsync(request.Dto.EntityId);
if (request.Dto.Decision == WorkflowDecision.Approved)
po.Approve(request.Dto.CompletedByUserId);
else
po.Reject(request.Dto.Comment);
await unitOfWork.SaveChangesAsync(ct);
return ApiResponse.Success();
}Workflow Definition JSON Format
Workflow definitions are stored as a JSON graph. The designer produces and consumes this format:
json
{
"code": "PO_APPROVAL",
"version": 1,
"steps": [
{
"id": "step-1",
"type": "Approval",
"nameAr": "مراجعة المدير المباشر",
"nameEn": "Line Manager Review",
"assigneeRole": "LineManager",
"timeoutHours": 48
},
{
"id": "step-2",
"type": "Condition",
"nameAr": "فحص المبلغ",
"nameEn": "Amount Check",
"condition": "Total > 50000",
"trueBranch": "step-3",
"falseBranch": "end-approved"
},
{
"id": "step-3",
"type": "Approval",
"nameAr": "موافقة المدير المالي",
"nameEn": "CFO Approval",
"assigneeRole": "CFO",
"timeoutHours": 72
}
],
"transitions": [
{ "from": "step-1", "to": "step-2", "on": "Approved" },
{ "from": "step-1", "to": "end-rejected", "on": "Rejected" }
]
}Azure Service Bus Topics
| Topic | Events Published | Subscribers |
|---|---|---|
workflow-events | StepAssigned, StepCompleted, StepTimedOut, InstanceCompleted, InstanceRejected | Workflows.Apis (self-subscribe), PlatformsWorker |
Configuration
json
// appsettings.json (Workflows.Apis)
{
"ConnectionStrings": {
"AdminConnection": "Server=...;Database=MicrotecAdmin;..."
},
"ServiceBus": {
"ConnectionString": "Endpoint=sb://...",
"TopicName": "workflow-events",
"SubscriptionName": "workflow-service"
},
"Services": {
"Notification": "https://mic-erp-be-notification.internal.<env>.azurecontainerapps.io"
},
"Keycloak": {
"Authority": "https://<keycloak-host>/realms/microtec",
"Audience": "account"
}
}Deployment
The Workflow service is listed in the pipeline service config:
Devops/azure/config/container-backend/services-config.json → "workflows"The WorkflowDesigner Blazor app is deployed separately and is not included in the main backend pipeline. It is managed manually or via its own pipeline in the WorkflowDesigner/ repository.