Skip to content

A lightweight, developer-first workflow engine built for .NET 8+. Define workflows using fluent DSL, handle state transitions, and manage tasks without external dependencies.

License

Notifications You must be signed in to change notification settings

anzawi/Meridian-Workflow

Repository files navigation

Meridian Workflow

A lightweight, developer-first workflow engine built for .NET 8+. Define workflows using fluent DSL, handle state transitions, and manage tasks without external dependencies.

NuGet Downloads License: Apache‑2.0

πŸ“š Table of Contents

❓ Why Should I Use Meridian?

Meridian is designed with developers in mind. It offers a clean, type-safe, and highly extensible workflow engine you can embed directly into your .NET 8+ applications without complex configuration or external dependencies.

  • βœ… Fully type-safe fluent DSL
  • πŸ” Powerful state transition management
  • 🧠 Hook system for business logic
  • πŸ” Fine-grained role/user-based action authorization
  • πŸ“ Built-in file handling and task generation
  • ☁️ Minimal dependencies and cloud-ready
  • 🧩 Clean architecture with plug-and-play extensions
  • ⚑ Suitable for microservices or monoliths

πŸ’‘ Note: Not all workflow engines are the same.
Meridian is focused on state-based, human-driven workflows (e.g., approvals, reviews),
unlike general-purpose engines such as Workflow Core or Elsa.
πŸ‘‰ See How is Meridian Different? for a detailed comparison.

Core Features

Workflow Definition & Execution

  • 🎯 Type-Safe Workflow DSL
    • Fluent API for intuitive workflow definitions
    • Compile-time type checking
    • Built-in validation
  • πŸ”„ State Management
    • Multi-step transitions
    • State entry/exit hooks
    • Auto-actions support
    • State-specific validation rules

Advanced Hook System

  • ⚑ Flexible Execution Models
    • Parallel hook execution for independent operations
    • Sequential execution for dependent operations
    • Critical and non-critical hook handling
  • πŸ”Œ Hook Types
    • Workflow-level hooks
    • State-specific hooks (OnEnter/OnExit)
    • Custom hook implementation support

Security & Authorization

  • πŸ” Fine-grained Access Control
    • Role-based authorization
    • Group-based permissions
    • Action-level security
    • User context awareness

File Management

  • πŸ“ Built-in File Handling
    • Pluggable storage providers
    • File upload/download operations
    • Attachment metadata management
    • Support for multiple storage backends
  • πŸ”§ Storage Configuration
    • Disabled storage option for non-file workflows
    • Custom provider implementation support

Task Management

  • πŸ“‹ Task Tracking
    • Automatic task generation
    • Status tracking
    • Assignment to users/roles/groups
    • Task lifecycle management

Data Handling & Persistence

  • πŸ’Ύ Flexible Storage
    • Multiple database support
    • Schema customization
    • Table prefix configuration
  • πŸ”„ Data Processing
    • Automatic validation
    • Change tracking
    • JSON-based serialization
    • Data comparison utilities

Monitoring & History

  • πŸ“Š Comprehensive Tracking
    • Detailed transition history
    • State change logging
    • User action tracking
    • Timestamp-based auditing

Error Handling

  • ⚠️ Specialized Exception Handling
    • Workflow-specific exceptions
    • Detailed error contexts
    • Operation-specific error types
    • Clear error messages

Architecture & Integration

  • πŸ—οΈ Clean Architecture
    • Dependency injection ready
    • Interface-based design
    • Extensible components
  • πŸ”Œ Easy Integration
    • ASP.NET Core support
    • Minimal dependencies
    • Cloud-ready design

Project Structure

The Meridian Workflow project follows a clean architecture pattern with the following structure:

  • src/
    • Meridian.Core/: Core domain logic and entities
    • Meridian.AspNetCore/: ASP.NET Core integration
    • Meridian.Application/: Application layer (use cases)
    • Meridian.Infrastructure/: Infrastructure implementations
  • tests/: Test projects
  • .git/: Git repository
  • README.md: Project documentation
  • LICENSE.txt: License information
  • Meridian.sln: Solution file

Project Organization

  • src/ - Contains all source code organized in different projects:

    • Meridian.Core: Contains the core domain logic, entities, and business rules
    • Meridian.AspNetCore: Provides integration with ASP.NET Core
    • Meridian.Application: Houses application-specific logic and use cases
    • Meridian.Infrastructure: Implements infrastructure concerns (persistence, external services)
  • tests/ - Contains all test projects

  • README.md - Main documentation file

  • LICENSE.txt - Project license information

  • Meridian.sln - Visual Studio solution file

πŸ“¦ Installation

dotnet add package Meridian.Workflow --version 1.1.0

πŸš€ Get Started in 5 Steps

1. Define Workflow Data Model

public class LeaveRequestData : IWorkflowData
{
    public string Reason { get; set; } = string.Empty;
    public int Days { get; set; }
}

2. Define the Workflow

public class LeaveRequestWorkflow : IWorkflowBootstrapper
{
    public void Register(IWorkflowDefinitionBuilder builder)
    {
        builder.Define<LeaveRequestData>("LeaveRequest", definition =>
        {
            definition.State("Pending", state =>
            {
                state.Action("Approve", "Approved");
                state.Action("Reject", "Rejected");
            });

            definition.State("Approved", state => state.IsCompleted());
            definition.State("Rejected", state => state.IsRejected());
        });
    }
  }

3. Register Meridian Workflow Engine

builder.Services.AddMeridianWorkflow(options =>
{
    options.Workflows =
    [
        new LeaveRequestWorkflow(),
    ];
    
    // other options...
});

4. Use the Engine

public class MyClass
{
    public MyClass(IWorkflowService<LeaveRequestData> leaveRequestWorkflow) 
    {
        // use the leaveRequestWorkflow to create, execute action, get history, get request, get logged-in user tasks, ...etc
    }
}

5. Visualize the Workflow (Optional)

workflowDefinition.PrintToConsole();

πŸ› οΈ Feature Deep Dive

βœ… Fluent DSL Definition

  • Why? Enables clean, reusable workflow definitions.
  • Examples:
builder.Define<LeaveRequestData>("LeaveRequest", def =>
{
    def.State("Pending", state =>
    {
        // State configuration
        state.Action("Approve", "Approved", action => 
        {
            // Action configuration
        });
        state.Action("Reject", "Rejected");
    });
});

Use Definition Templates:

Definition templates help you create reusable workflow patterns and keep your workflow definitions DRY (Don't Repeat Yourself). They are particularly useful when you have common states, actions, or behaviors that appear in multiple workflows.

πŸ”‘ Key Benefits

  • ♻️ Reusable workflow patterns
  • 🎯 Consistent behavior across workflows
  • πŸ“ Reduced code duplication
  • πŸ› οΈ Easy maintenance
  • 🧩 Modular workflow design

πŸ“š Common Use Cases

  1. Common States: Reuse standard states like Approved, Rejected, or UnderReview
  2. Standard Actions: Apply consistent actions like approve/reject patterns
  3. Security Templates: Reuse role and permission configurations
  4. Hook Templates: Apply common hooks across workflows
public static class GeneralWorkflowTemplates
{
    public static IWorkflowDefinitionBuilder<LeaveRequestData> WithCommonStates(
        this IWorkflowDefinitionBuilder<LeaveRequestData> workflowDefinition)
    {
        workflowDefinition
            .State(GeneralWorkflowStates.Rejected, state =>
            {
                state.SendToSmartServicesHook();
            })
            .State(GeneralWorkflowStates.Updating, state =>
            {
                state.SendToSmartServicesHook();
            })
            .State(GeneralWorkflowStates.Approved, state =>
            {
                state.SendToSmartServicesHook();
                state.IsCompleted();
            });

        return workflowDefinition;
    }
    
     public static IStateBuilder<LeaveRequestData> WithStandardRejectionActions(
        this IStateBuilder<LeaveRequestData> state)
    {
        state.Action(GeneralWorkflowActions.Reject, GeneralWorkflowStates.Rejected);
        state.Action(GeneralWorkflowActions.Incomplete, GeneralWorkflowStates.Updating);
        return state;
    }
}

// Usage
public class InitialApprovalWorkflow : IWorkflowBootstrapper
{
    public void Register(IWorkflowDefinitionBuilder builder)
    {
        builder.Define<LeaveRequestData>("InitialApproval", definition =>
        {
            definition
                .WithCommonStates();
                .State(GeneralWorkflowStates.UnderReview, state =>
                {
                    state.WithStandardRejectionActions();
                })
        });
    }
}

// Hooks template
public static class CommonHooks
{
    public static WorkflowState<LeaveRequestData> SendToSmartServicesHook(
        this WorkflowState<LeaveRequestData> state)
    {
        state.AddHook(new WorkflowHookDescriptor<LeaveRequestData>
        {
            Hook = new SendRequestToSmartServices(),
            IsAsync = true,
        }, StateHookType.OnStateEnter);

        return state;
    }
}

🧠 Hooks (Event Handlers)

  • Purpose: Execute logic during request lifecycle (create, transition, entry/exit).
  • Types:
    • Workflow Definition
      • OnCreateHooks (when a new request is created)
      • OnTransitionHooks (when request transitions)
    • State
      • OnEnterHooks (when request enters the state)
      • OnExitHooks (when request exits the state)
    • Action
      • OnExecuteHooks (when a user takes an action)

You can use the AddHook extension method in three ways:

  1. Pass a WorkflowHookDescriptor<TData> (full control)
  2. Pass a class implementing IWorkflowHook<TData>
  3. Pass a lambda delegate Func<WorkflowContext<TData>, Task>

βœ… Add Hook to Workflow (using WorkflowHookDescriptor)

workflowDefinition.AddHook(new WorkflowHookDescriptor<LeaveRequestData>
{
    Hook = new NewRequestWasCreated(),
    IsAsync = false,
    LogExecutionHistory = true
}, WorkflowHookType.OnRequestCreated);

βœ… Add Hook to Workflow (using class)

workflowDefinition.AddHook(
    new NewRequestWasCreated(),
    cfg => {
        cfg.IsAsync = false;
    },
    WorkflowHookType.OnRequestCreated);

βœ… Add Hook to Workflow (using lambda)

workflowDefinition.AddHook(
    async ctx =>
    {
        Console.WriteLine($"New request for {ctx.InputData?.EmployeeName}");
        await Task.CompletedTask;
    },
    cfg => {
        cfg.IsAsync = true;
        cfg.LogExecutionHistory = false;
    },
    WorkflowHookType.OnRequestCreated);

βœ… Add Hook to State (using WorkflowHookDescriptor)

workflowDefinition.State("StateName", state =>
{
    state.AddHook(new WorkflowHookDescriptor<LeaveRequestData>
    {
        Hook = new SendRequestToSmartServices(),
        IsAsync = true,
    }, StateHookType.OnStateEnter);
});

βœ… Add Hook to State (using class)

workflowDefinition.State("StateName", state =>
{
    state.AddHook(
        new SendRequestToSmartServices(),
        cfg => {
            cfg.IsAsync = true;
        },
        StateHookType.OnStateEnter);
});

βœ… Add Hook to State (using lambda)

workflowDefinition.State("StateName", state =>
{
    state.AddHook(
        async ctx =>
        {
            Console.WriteLine("Entered state");
            await Task.CompletedTask;
        },
        cfg => {
            cfg.IsAsync = true;
        },
        StateHookType.OnStateEnter);
});

βœ… Add Hook to Action (using WorkflowHookDescriptor)

workflowDefinition.State("StateName", state =>
{
    state.Action("actionName", "targetState", action =>
    {
        action.AddHook(new WorkflowHookDescriptor<LeaveRequestData>
        {
            Hook = new DoSomething(),
            IsAsync = true,
        });
    });
});

βœ… Add Hook to Action (using class)

workflowDefinition.State("StateName", state =>
{
    state.Action("actionName", "targetState", action =>
    {
        action.AddHook(
            new DoSomething(),
            cfg => {
                cfg.IsAsync = true;
            });
    });
});

βœ… Add Hook to Action (using lambda)

workflowDefinition.State("StateName", state =>
{
    state.Action("actionName", "targetState", action =>
    {
        action.AddHook(
            async ctx =>
            {
                Console.WriteLine("Action executed");
                await Task.CompletedTask;
            },
            cfg => {
                cfg.IsAsync = true;
            });
    });
});

🧩 Define a Hook Class

public class NewRequestWasCreated : IWorkflowHook<LeaveRequestData>
{
    public Task ExecuteAsync(WorkflowContext<LeaveRequestData> context)
    {
        Console.WriteLine("Hook class executed");
        return Task.CompletedTask;
    }
}

Use Builtin Hooks (reusable hooks)

Meridian Workflow provides built-in reusable hooks that simplify common workflow behaviors. One such hook is:

🧩 CompareDataAndLogHook<TData>

This hook compares the existing request data with the new input data during a transition and logs all field-level changes to the request history. It's useful for audit trails and understanding how data evolved over time.

πŸ”§ Usage

Add the hook to a request transition:

definition.AddCompareDataAndLogHistory();

Or attach it manually to any hook-supported point (e.g., action execution):

action.AddHook(new WorkflowHookDescriptor<LeaveRequestData> 
{
    Hook = new CompareDataAndLogHook<LeaveRequestData>(),
    Mode = HookExecutionMode.Parallel,
    IsAsync = false,
    LogExecutionHistory = false,
});

πŸ” Action Authorization

Meridian Workflow lets you define who can see or perform each action by assigning allowed users, roles, or groups.

This ensures that only authorized participants can interact with workflow actions during execution.


πŸ”§ Basic Assignment

Use these methods for simple access control:

workflowDefinition
    .State("StateName", state => 
    {
        state.Action("actionName", "targetState", action => 
        {
            action.AssignToGroups("group1", "group2");
            action.AssignToUsers("user1", "user2");
            action.AssignToRoles("role1", "role2", "role3");
        });
    });

βœ… A user is authorized if they match any of the assigned roles, users, or groups.


🧠 Advanced Authorization Rules (Optional)

For more complex scenarios (e.g., logical conditions, exclusions, or nested rules), use a fluent expression builder:

state.Action("Approve", "Approved", action => action.AssignTo(rules => rules
    .Role("manager", "supervisor")
    .Or(b => b.Group("finance_team"))
    .And(b => b.Not(x => x.Role("intern")))
));

πŸ”Ž Rule Evaluation Logic

  • Conditions are evaluated at runtime against the current user.
  • The user is authorized if:
    • They match any assigned user/role/group via the basic API
    • OR they satisfy the advanced rule expression

βœ… Best Practices

  • Use .AssignToRoles, .AssignToUsers, .AssignToGroups for straightforward workflows.
  • Use .AssignTo(...) when logic includes combinations or exceptions.
  • Both methods are supported and are combined using OR logic.
action.AssignToUsers("john");
action.AssignTo(r => r.Role("manager").Not(x => x.Role("blocked")));

πŸ”’ The above means:

  • Allow if user is "john"
  • OR if user has role "manager" AND is not in role "blocked"

Auto Actions

Mark an action to be taken by condition

workflowDefinition
    .State("StateName", state => 
    {
        state.Action("actionName", "targetState", action => 
        {
            action.IsAuto = true;
            action.Condition = data => data.Department == "IT" && data.Priority > 10;
        });
    });

Conditional Actions

Meridian Workflow supports conditional transitions that dynamically determine the next state based on the data being processed. This enables flexible workflows that adapt to runtime conditions.

Basic Usage

state.Action("Approve", "PendingManagerApproval", action => action
    .AssignToRoles("Manager")
    .When(data => data.Amount > 10000, "PendingDirectorApproval")
    .When(data => data.Amount > 5000, "PendingSupervisorApproval")
);

What This Does

  • If Amount > 10000, transitions to "PendingDirectorApproval"
  • If Amount > 5000 and <= 10000, transitions to "PendingSupervisorApproval"
  • Otherwise, transitions to the default state "PendingManagerApproval"

How It Works

  • Conditions are evaluated in the order they are defined.
  • The first condition that returns true determines the transition.
  • If no condition matches, the default transition is used.

Best Practices

1. Order Matters

Place more specific conditions before more general ones:

.When(data => data.Amount > 10000, "HighValueApproval") // Specific
.When(data => data.Amount > 1000, "StandardApproval")   // General

2. Provide a Clear Default

Always specify a meaningful default state:

state.Action("Review", "StandardReview", action => action
    .When(data => data.IsUrgent, "ExpeditedReview")
    // Falls back to "StandardReview" if not urgent
);

3. Avoid Overlapping Conditions

Ensure that conditions are mutually exclusive:

// Good - Conditions don't overlap
.When(data => data.Days > 30, "ExtendedLeave")
.When(data => data.Days > 15 && data.Days <= 30, "StandardLeave")

// Bad - Overlapping conditions
.When(data => data.Days > 15, "StandardLeave")
.When(data => data.Days > 30, "ExtendedLeave") // Never reached!

Transition Tables (Multi-Condition Routing)

For complex conditional transitions, Meridian supports a more expressive syntax using a transition table. This allows you to define multiple branching paths in a single statement.

Example

state.Action("Approve", action => action.TransitionTo(
    (data => data.Amount > 10000, "PendingDirectorApproval", "Amount > 10000"),
    (data => data.Amount > 5000, "PendingSupervisorApproval", "Amount > 5000"),
    (data => true, "PendingManagerApproval", "Default")
));

What This Does

  • Transitions to "PendingDirectorApproval" if Amount > 10000
  • Otherwise, transitions to "PendingSupervisorApproval" if Amount > 5000
  • Otherwise, transitions to "PendingManagerApproval" (default case)

Benefits

  • Centralizes all conditional transitions in a single block
  • Improves readability and maintainability
  • Enables optional labeling for documentation and debugging

Optional Overload

You can omit labels if not needed:

state.Action("Approve", action => action.TransitionTo(
    (data => data.Amount > 10000, "PendingDirectorApproval"),
    (data => data.Amount > 5000, "PendingSupervisorApproval"),
    (data => true, "PendingManagerApproval")
));

How Transition Tables Work

  • All conditions are evaluated in order.
  • The first match determines the next state.
  • Labels (optional) are used for visual tools and logs.
  • Transition tables override .When(...) if both are defined.

Best Practices for Transition Tables

  • Keep the fallback case data => true as the last rule.
  • Use labels to describe business rules for better debugging and documentation.
  • Do not mix .When(...) and .TransitionTo(...) unless intentional β€” only one will be used.
// Good
.TransitionTo(
    (d => d.Type == "Annual", "AnnualReview", "Annual leave"),
    (d => d.Type == "Sick", "MedicalReview", "Sick leave"),
    (d => true, "DefaultReview", "Fallback")
)
// Avoid this unless you know what you're doing
.When(d => d.Type == "Urgent", "Expedited")
.TransitionTo((d => true, "Standard"))

Validate Model in Action

Meridian Workflow performs automatic model validation before executing an action to ensure data integrity.

πŸ”§ Disabling Auto-Validation

To disable automatic validation on a specific action:

workflowDefinition
    .State("StateName", state => 
    {
        state.Action("actionName", "targetState", action => 
        {
            action.DisableAutoValidation();
        });
    });

✨ Defining Custom Validation Logic

You can also define custom validation rules using the WithValidation method. This allows fine-grained, context-specific validation before the action executes.

action.WithValidation(data =>
{
    var errors = new List<string>();
    if (string.IsNullOrEmpty(data.Department))
    {
        errors.Add("Department cannot be empty");
    }
    
    return errors;
});

πŸ“Ž File Attachments

No extra effort is needed from developers. Just use:

public class LeaveRequestData : IWorkflowData
{
    public WorkflowFile<WorkflowFileAttachment> MedicalReport { get; set; } = new();
}

The engine:

  • Detects attachments
  • Uploads via IWorkflowFileStorageProvider
  • Replaces them with references
public interface IWorkflowFileStorageProvider<TReference>
{
    Task<TReference> UploadAsync(WorkflowFileAttachment attachment);
}

You implement this to store on disk, S3, or DB.

Example

public class AttachmentReference
{
    public Guid AttachmentId { get; set; }
    public string Path { get; set; } = string.Empty;
    public string? Source { get; set; }
}

public class WorkflowFileStorageProvider : IWorkflowFileStorageProvider<AttachmentReference>
{
    public async Task<AttachmentReference> UploadAsync(IWorkflowAttachment attachmentFile)
    {
        return await Task.FromResult(new AttachmentReference
        {
            Path = "File path",
            Source = "File source",
            AttachmentId = Guid.NewGuid(),
        });
    }
}

Register the File Storage Provider

builder.Services.AddMeridianWorkflow(options =>
{
    options.EnableAttachmentProcessor = true; // Optional, true by default
    options.SetFileStorageProvider(typeof(WorkflowFileStorageProvider));
    // other options...
});

πŸ’‘ Set EnableAttachmentProcessor = false to disable built-in attachment processing if you need full control.

Database Configuration (EF Core support)

Meridian Workflow supports Entity Framework Core out of the box, NOT FULLY TESTED with all providers:

You can integrate any EF Core-supported provider (PostgreSQL, SQLite, Oracle, etc.) by configuring the underlying DbContext.

builder.Services.AddMeridianWorkflow(options =>
{
     options.ConfigureDb(db =>
   {
       db.Use(dbOptions => dbOptions.UseInMemoryDatabase("WorkflowTestDb"));
       // db.Use(dbOptions => dbOptions.UseSqlServer("WorkflowTestDb"));
       db.TablesPrefix = "Meridian_"; // Optional: Set custom table prefix
       db.Schema = "MySchema"; // Optional: Set sechma (required with oracle)
   });
});

πŸ”’ Schema and table prefixing allow you to isolate workflow data in shared databases.

Tasks Per Action

Automatically creates a WorkflowRequestTask per action in a state.

public class WorkflowRequestTask
{
    public string RequestId { get; set; }
    public string Action { get; set; }
    public string State { get; set; }
    public List<string> AssignedToRoles { get; set; }
    public WorkflowTaskStatus Status { get; set; }
}

On each transition:

  • βœ… Old tasks marked as completed
  • βœ… New tasks created for next state's actions

πŸ§ͺ Visual Debugging (Console Flowchart)

workflowDefinition
    .State("StateName", state => 
    {
    })
    // Use this extension
    .PrintToConsole();

Output

══════════════════════════════════════════
Workflow: LeaveRequest
══════════════════════════════════════════

State: Pending
 β”œβ”€ Approve β†’ Approved
 └─ Reject  β†’ Rejected

State: Approved
State: Rejected

πŸ”§ IWorkflowService

Provides all operations to manage and execute workflow requests for a specific workflow type.


πŸ“„ Available Methods

Method Description
CreateRequestAsync Creates a new workflow request instance
DoActionAsync Executes a specific action on a workflow request
GetRequestAsync Retrieves a workflow request by ID
GetUserTasksAsync Gets requests assigned to a specific user
GetAvailableActions Gets available actions for a request
GetCurrentState Gets the current state of a request
GetRequestHistoryAsync Returns transition history for a request
GetRequestWithHistoryAsync Returns request and its history together

🧩 CreateRequestAsync

Description:
Creates a new workflow request for the current workflow definition.

Parameters:

Name Type Required Description
inputData TData βœ… Initial workflow input data
createdBy string βœ… ID of the user creating request

Example:

await workflow.CreateRequestAsync(new LeaveRequestData { LeaveType = "Annual" }, "user123");

🧩 DoActionAsync

Description:
Executes a specific action (e.g., "Approve", "Reject") on an existing request.

Overloads:

  • With only action and request ID
  • With additional data to update the workflow

Parameters:

Name Type Required Description
requestId Guid βœ… ID of the request to act on
action string βœ… Action name to execute
performedBy string βœ… User performing the action
userRoles List βœ… User's roles
userGroups List βœ… User's groups
data TData? ❌ Optional updated request data

Example:

await workflow.DoActionAsync(requestId, "Submit", "user123", roles, groups);  
await workflow.DoActionAsync(requestId, "Update", "user123", roles, groups, newData);

🧩 GetRequestAsync

Description:
Returns the workflow request by ID.

Parameters:

Name Type Required Description
requestId Guid βœ… ID of the workflow request

Example:

var request = await workflow.GetRequestAsync(id);

🧩 GetUserTasksAsync

Description:
Returns all requests where the user has available actions based on role/group.

Parameters:

Name Type Required Description
userId string βœ… ID of the user
userRoles List βœ… List of user's roles
userGroups List βœ… List of user's groups

Example:

var tasks = await workflow.GetUserTasksAsync("user123", roles, groups);

🧩 GetAvailableActions

Description:
Returns the list of actions the user can perform on the request.

Overloads:

  • By passing the request object
  • By passing the request ID

Parameters (Overload 1):

Name Type Required Description
request WorkflowRequestInstance βœ… Workflow request object
userId string? ❌ ID of the user
userRoles List? ❌ Roles of the user
userGroups List? ❌ Groups of the user

Parameters (Overload 2):

Name Type Required Description
requestId Guid βœ… ID of the request
userId string? ❌ ID of the user
userRoles List? ❌ Roles of the user
userGroups List? ❌ Groups of the user

Examples:

workflow.GetAvailableActions(request, "user", roles);  
await workflow.GetAvailableActions(requestId, "user", roles);

🧩 GetCurrentState

Description:
Returns the current state of the specified workflow request.

Parameters:

Name Type Required Description
request WorkflowRequestInstance βœ… Request to check

Example:

var state = workflow.GetCurrentState(request);

🧩 GetRequestHistoryAsync

Description:
Returns the full transition history for a request.

Parameters:

Name Type Required Description
requestId Guid βœ… ID of the request

Example:

var history = await workflow.GetRequestHistoryAsync(request.Id);

🧩 GetRequestWithHistoryAsync

Description:
Returns both request and its full transition history.

Parameters:

Name Type Required Description
requestId Guid βœ… ID of the request

Example:

var full = await workflow.GetRequestWithHistoryAsync(request.Id);

πŸ“Š Architecture

  • Clean separation of Domain / Application / Infrastructure
  • Plug-and-play registry-based engine resolver
  • Reflection-based workflow registry
  • Supports future runtime definitions (JSON)

πŸ”Œ Extending Meridian Workflow

Feature You Can Plug In...
File Upload IWorkflowFileStorageProvider<TReference>
Hook Execution Implement IWorkflowHook<TData>
Custom Transition Logic Add logic in hook or conditions

πŸ“ Sample Projects

βœ… Leave Request

🚧 More coming soon...

🧰 Use Cases

Meridian Workflow can handle a wide variety of business scenarios:

  • πŸ“… Leave Request Approval

    • Simple multi-step approval with hooks and auto actions
  • πŸ“„ Document Review Workflow

    • Handle file uploads, rejections, and resubmissions
  • πŸ‘₯ HR Onboarding

    • Automate onboarding steps with role-based task assignments
  • πŸ› οΈ Support Ticket Lifecycle

    • Escalation, auto-routing, SLA tracking
  • 🧾 Procurement Approvals

    • Include price validation, PDF verification, and user-specific approvals
  • πŸ”„ Multi-Level Reviews

    • Nested approval chains with sub-workflows and task delegation
  • MORE AND MORE!!

πŸ” How is Meridian Different?

Meridian Workflow is not a replacement for heavy orchestration engines like Elsa, nor is it a drop-in alternative to Workflow Core. Each serves a different use case and design philosophy.

Meridian vs Workflow Core

Feature Meridian Workflow Core
Workflow Type State Machine (User/Approval-driven) Step-based/Flow-based
DSL Fully type-safe Fluent API Fluent + JSON
Use Case Focus Approval workflows, human interaction, business tasks General-purpose orchestration
Task Handling Built-in task system with roles/users/groups Requires custom implementation
Authorization Built-in role/group/user-based action authorization Not built-in
Extensibility Hooks, Templates, Pluggable Features Middleware/step extensions
Simplicity & Dev Focus Lightweight, zero-config, developer-first More generic, more boilerplate

Meridian vs Elsa

Feature Meridian Elsa Workflows
Workflow Type Human Workflow / Approval-based Activity-based Orchestration
Designer UI ❌ Not available βœ… Powerful designer (optional)
DSL βœ… Fluent API (C#) C# or JSON
Persistence Model Optional / Lightweight Required (workflow instance tracking)
Approval & Action Model βœ… States, Actions, Users, Tasks ❌ Not native
Suitable For Leave requests, ticket lifecycle, business processes Long-running workflows, integrations

Meridian is designed for human-centric workflows like approvals, reviews, multi-step user processes, and role-based transitions β€” all using clean, extensible code without a designer or runtime engine.

πŸ“ Roadmap

The following features are planned or under consideration for future releases:

  • Timeout & Escalation Support
  • Delegation & Reassignment
  • Multi-request Relationships
  • Sub-Workflows & Nested Workflows
  • Conditional Transitions Upgrade
    (Take a look at Conditional Actions in the Status / Limitations section)
  • JSON-based Workflow Definitions

🚧 More coming soon...

πŸ‘ Contributing

Want to help improve Meridian Workflow?

  • Report issues
  • Submit new DSL extensions
  • Create advanced real-world samples
  • Help to add reusable hook
  • Help to add Timeout / Escalation
  • Help to add Delegation / Reassign
  • Help to add Multi-Request Relationship
  • Help to add Sub-Workflows
  • Help to convert Fluent-DSL to JSON-DSL

⚠️ Status / Limitations

Unit tests are not fully completed.
Contributions to improve coverage and test edge cases are welcome.

  • Conditional Actions:
    • Conditions are evaluated in definition order.
    • No priority system for condition evaluation.
    • No built-in conflict detection for overlapping conditions.
    • No support for transition-specific validation or hooks.

πŸ“„ License

Apache License 2.0. Free for use in open-source and commercial applications. Includes conditions for redistribution and attribution.

https://opensource.org/licenses/Apache-2.0

About

A lightweight, developer-first workflow engine built for .NET 8+. Define workflows using fluent DSL, handle state transitions, and manage tasks without external dependencies.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages