Skip to content

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

PropertyValue
ServiceWorkflows.Apis
CAE placementPrivate CAE
Gateway route prefix/workflow-apis/
SourcePlatforms/Src/InfrastructureServices/Workflow/
DesignerWorkflowDesigner/ (Blazor Server)
Event busAzure Service Bus
Auth realmmicrotec (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.Apis

Workflow 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 TypeEnumDescription
ApprovalStepType.ApprovalRequires a user to approve or reject
NotificationStepType.NotificationSends a message via Notification service
ConditionStepType.ConditionEvaluates a rule and branches
AutoActionStepType.AutoActionExecutes a callback to the originating service
ParallelStepType.ParallelRuns multiple steps simultaneously
DelayStepType.DelayWaits for a defined duration

Runtime Execution Flow


API Endpoints

Workflow Definitions

MethodRouteOperation
GET/WorkflowDefinitionsList definitions
GET/WorkflowDefinitions/{Id}Get by ID including JSON graph
POST/WorkflowDefinitionsSave new definition
PUT/WorkflowDefinitionsUpdate definition (creates new version)
POST/WorkflowDefinitions/{Id}/ActivateActivate a version

Workflow Instances

MethodRouteOperation
GET/WorkflowInstancesList instances (filter by entity, status)
GET/WorkflowInstances/{Id}Get instance with steps
POST/WorkflowInstances/TriggerStart a new workflow instance
POST/WorkflowInstances/{Id}/ApproveApprove current step
POST/WorkflowInstances/{Id}/RejectReject current step
POST/WorkflowInstances/{Id}/CancelCancel running instance

My Approvals

MethodRouteOperation
GET/MyApprovalsPending approvals for current user
GET/MyApprovals/HistoryCompleted 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

TopicEvents PublishedSubscribers
workflow-eventsStepAssigned, StepCompleted, StepTimedOut, InstanceCompleted, InstanceRejectedWorkflows.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.

Internal Documentation — Microtec Platform Team