Chd.Workflow 1.0.5

dotnet add package Chd.Workflow --version 1.0.5
                    
NuGet\Install-Package Chd.Workflow -Version 1.0.5
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Chd.Workflow" Version="1.0.5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Chd.Workflow" Version="1.0.5" />
                    
Directory.Packages.props
<PackageReference Include="Chd.Workflow" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Chd.Workflow --version 1.0.5
                    
#r "nuget: Chd.Workflow, 1.0.5"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Chd.Workflow@1.0.5
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Chd.Workflow&version=1.0.5
                    
Install as a Cake Addin
#tool nuget:?package=Chd.Workflow&version=1.0.5
                    
Install as a Cake Tool

Chd.Workflow 🚀

NuGet .NET License: MIT

Chd (Cleverly Handle Difficulty) library helps you cleverly handle difficulty, write code quickly, and keep your application stable. Chd.Workflow is a lightweight, tree-based workflow engine for .NET. Design workflows visually, execute backend actions on transitions, and build dynamic forms — all with minimal code.


📑 Table of Contents


✨ Features

Feature Description
🌳 Tree-based Structure Hierarchical node design with parent-child relationships
🔄 Transition Actions Execute backend methods when transitioning between states
📝 Dynamic Forms Define form fields per node with 15+ field types
🔐 Role-based Access Control who can execute which transitions
📊 Condition Engine Route workflows based on data conditions
🏢 Multi-tenant Built-in tenant isolation
🐘 PostgreSQL Support Built-in EF Core repository with FluentMigrator
🔷 SQL Server Support Built-in EF Core repository with FluentMigrator
💾 Pluggable Storage InMemory default, or implement custom IWorkflowRepository
🌐 REST API Auto-registered minimal API endpoints
Action Caching Assembly scanned once at startup, actions cached
🧩 Controller Method Actions Register legacy controller/service methods as workflow actions via attribute
🔒 Runtime SQL Isolation optionsQuery executes on backend; runtime state returns resolved options without leaking raw SQL

💡 Why Chd.Workflow?

Before (Traditional Approach)

// Scattered if-else statements across your codebase
public void ProcessRequest(Request request, string action)
{
    if (request.Status == "Draft")
    {
        if (action == "submit")
        {
            if (request.Amount > 10000)
            {
                request.Status = "PendingGMApproval";
                SendNotificationToGM();
            }
            else
            {
                request.Status = "PendingManagerApproval";
                SendNotificationToManager();
            }
            SaveToDatabase();
        }
    }
    else if (request.Status == "PendingManagerApproval")
    {
        if (action == "approve")
        {
            request.Status = "Approved";
            UpdateBudget();
            SendApprovalEmail();
            // ... 50 more lines
        }
        else if (action == "reject")
        {
            // ... 30 more lines
        }
    }
    // ... hundreds more lines of if-else
}

After (With Chd.Workflow)

// 1. Define workflow visually in Designer (or JSON)
// 2. Create simple action classes

[WorkflowAction("approve-request", DisplayName = "Approve Request")]
public class ApproveRequestAction : IWorkflowAction
{
    private readonly IDbContext _db;
    private readonly IEmailService _email;

    public ApproveRequestAction(IDbContext db, IEmailService email)
    {
        _db = db;
        _email = email;
    }

    public async Task<WorkflowActionResult> ExecuteAsync(WorkflowActionContext context)
    {
        var request = await _db.Requests.FindAsync(context.FormData["requestId"]);
        request.Status = "Approved";
        request.ApprovedBy = context.UserId;
        
        await _db.SaveChangesAsync();
        await _email.SendApprovalEmail(request);
        
        return WorkflowActionResult.Ok();
    }
}

// 3. Bind action to transition in Designer UI - Done!

Benefits

Aspect Before After
Workflow Changes Modify code, deploy Update in Designer, no deploy
New Approvals Write new if-else Add node + transition in UI
Debugging Search through if-else Check workflow instance history
Testing Complex integration tests Unit test each action
Onboarding Read hundreds of lines View visual workflow

📦 Installation

dotnet add package Chd.Workflow

Supported Frameworks:

  • .NET 8.0
  • .NET 9.0

🚀 Quick Start

1. Register Services

// Program.cs
using Chd.Workflow.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Register workflow actions from your assembly (FIRST)
builder.Services.AddWorkflowActionsFromCallingAssembly();

// Add workflow services
builder.Services.AddWorkflow();

var app = builder.Build();

// Map workflow API endpoints
app.UseWorkflow();

app.Run();

2. Create a Workflow Action

using Chd.Workflow.Attributes;
using Chd.Workflow.Interfaces;

[WorkflowAction("approve-leave", DisplayName = "Approve Leave", Category = "HR")]
public class ApproveLeaveAction : IWorkflowAction
{
    public Task<WorkflowActionResult> ExecuteAsync(WorkflowActionContext context)
    {
        Console.WriteLine($"Leave approved by {context.UserId}");
        
        return Task.FromResult(WorkflowActionResult.Ok(new Dictionary<string, object?>
        {
            ["approvedAt"] = DateTime.UtcNow,
            ["approvedBy"] = context.UserId
        }));
    }
}

3. Use the API

# Get available actions (for Designer dropdown)
GET /api/workflow/actions

# Create workflow instance
POST /api/workflow/instances
{
    "definitionId": "leave-request",
    "data": { "employeeId": "123", "days": 5 }
}

# Execute transition
POST /api/workflow/instances/{id}/transition
{
    "action": "approve",
    "data": { "comment": "Approved!" }
}

# Get current state
GET /api/workflow/instances/{id}/state

📚 Core Concepts

Workflow Definition

A workflow definition is a template that describes the flow structure.

public class WorkflowDefinition
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string? Description { get; set; }
    public int Version { get; set; }
    public bool IsActive { get; set; }
    public Node RootNode { get; set; }      // Tree structure starts here
    public string? TenantId { get; set; }   // Multi-tenant support
}

Nodes

Nodes represent states/steps in your workflow.

public class Node
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string? Title { get; set; }
    public NodeType Type { get; set; }        // Start, Task, Decision, End
    public List<Node> Children { get; set; }  // Child nodes (tree structure)
    public List<Field> Fields { get; set; }   // Form fields for this node
    public List<Transition> Transitions { get; set; }
    public FormType FormType { get; set; }    // Dynamic, External, Component, None
    public string? FormUrl { get; set; }      // For external forms
    public string? DataQuery { get; set; }    // SQL to load form data
}

Node Types: | Type | Description | |------|-------------| | Start | Entry point of workflow | | Task | User action required | | Decision | Automatic routing based on conditions | | End | Terminal state |

Transitions

Transitions define how to move from one node to another.

public class Transition
{
    public string Id { get; set; }
    public string Action { get; set; }         // "approve", "reject", "submit"
    public string? Label { get; set; }         // Button label
    public string TargetNodeId { get; set; }   // Where to go
    public string? Condition { get; set; }     // When this transition is valid
    public string? ActionHandler { get; set; } // Backend method to execute
    public List<string> AllowedRoles { get; set; }
    public bool RequiresComment { get; set; }
}

Fields (Dynamic Forms)

Define form fields that appear when a node is active.

public class Field
{
    public string Name { get; set; }
    public string? Label { get; set; }
    public FieldType Type { get; set; }       // Text, Number, Date, Dropdown, etc.
    public bool Required { get; set; }
    public string? Placeholder { get; set; }
    public string? ValidationPattern { get; set; }
    public string? OptionsQuery { get; set; } // SQL for dropdown options
    public List<FieldOption>? Options { get; set; }
}

Field Types: Text, TextArea, Number, Decimal, Date, DateTime, Time, Checkbox, Radio, Dropdown, MultiSelect, File, Image, RichText, Hidden


⚙️ Workflow Actions (Backend Methods)

The core feature of Chd.Workflow is binding backend methods to transitions.

Creating an Action

using Chd.Workflow.Attributes;
using Chd.Workflow.Interfaces;

[WorkflowAction("send-notification", DisplayName = "Send Notification", Category = "Communication")]
[WorkflowAuthorize(Roles = "Admin,Manager")]  // Optional: Role restriction
public class SendNotificationAction : IWorkflowAction
{
    private readonly INotificationService _notifications;
    private readonly ILogger<SendNotificationAction> _logger;

    // Constructor injection works!
    public SendNotificationAction(
        INotificationService notifications, 
        ILogger<SendNotificationAction> logger)
    {
        _notifications = notifications;
        _logger = logger;
    }

    public async Task<WorkflowActionResult> ExecuteAsync(WorkflowActionContext context)
    {
        try
        {
            // Access form data
            var message = context.FormData["message"]?.ToString();
            var recipientId = context.FormData["recipientId"]?.ToString();
            
            // Access workflow context
            _logger.LogInformation(
                "Sending notification from node {Node} by user {User}",
                context.CurrentNode.Title,
                context.UserId);

            await _notifications.SendAsync(recipientId, message);

            // Return success with optional output data
            return WorkflowActionResult.Ok(new Dictionary<string, object?>
            {
                ["notificationSentAt"] = DateTime.UtcNow,
                ["notificationId"] = Guid.NewGuid().ToString()
            });
        }
        catch (Exception ex)
        {
            // Return failure - transition will NOT happen
            return WorkflowActionResult.Fail($"Failed to send notification: {ex.Message}");
        }
    }
}

Using Existing Controller/Service Methods

For legacy integrations, you can expose existing methods as workflow actions without implementing IWorkflowAction directly.

using Chd.Workflow.Attributes;
using Chd.Workflow.Interfaces;

public class LegacyBudgetController
{
    [WorkflowControllerAction(
        "legacy-budget-approve",
        DisplayName = "Legacy Budget Approve",
        Category = "Controller Actions",
        InputType = typeof(BudgetApproveRequest))]
    public async Task<WorkflowActionResult> ApproveFromLegacy(WorkflowActionContext context)
    {
        // Reuse existing business flow
        return WorkflowActionResult.Ok();
    }
}

Method signature rules:

  • Must accept exactly one parameter: WorkflowActionContext
  • Must return WorkflowActionResult or Task<WorkflowActionResult>

WorkflowActionContext

The context provides access to all workflow data:

public class WorkflowActionContext
{
    public WorkflowInstance Instance { get; }      // Current instance
    public WorkflowDefinition Definition { get; }  // Workflow definition
    public Node CurrentNode { get; }               // Current node
    public Node? TargetNode { get; }               // Node we're transitioning to
    public Transition? Transition { get; }         // The transition being executed
    public IDictionary<string, object?> FormData { get; }  // Submitted form data
    public string? UserId { get; }                 // Current user
    public IReadOnlyList<string> UserRoles { get; }
    public string? Comment { get; }                // Optional comment
    public IServiceProvider ServiceProvider { get; }
    public CancellationToken CancellationToken { get; }
}

Registering Actions

// Program.cs

// Option 1: Register from calling assembly
builder.Services.AddWorkflowActionsFromCallingAssembly();

// Option 2: Register from specific assemblies
builder.Services.AddWorkflowActions(
    typeof(ApproveLeaveAction).Assembly,
    typeof(AnotherAction).Assembly
);

// Then add workflow services
builder.Services.AddWorkflow();

Binding Actions to Transitions

Actions are bound to transitions via the ActionHandler property:

{
  "action": "approve",
  "targetNodeId": "approved-node",
  "actionHandler": "approve-leave"
}

Or via the visual Designer UI (dropdown selection).

Execution Flow

User clicks "Approve" button
         ↓
POST /api/workflow/instances/{id}/transition
         ↓
Find transition with action="approve"
         ↓
Check condition (if any)
         ↓
ActionHandler = "approve-leave"?
    ├── YES → Execute ApproveLeaveAction.ExecuteAsync()
    │         ├── Success → Continue to target node
    │         └── Fail → Return error, stay at current node
    └── NO → Just transition to target node
         ↓
Update instance.CurrentNodeId
         ↓
Save to repository
         ↓
Return updated WorkflowState

🌐 API Endpoints

All endpoints are prefixed with /api/workflow by default.

Definitions

Method Endpoint Description
GET /definitions List all definitions
GET /definitions/{id} Get definition by ID
POST /definitions Create definition
PUT /definitions/{id} Update definition
DELETE /definitions/{id} Delete definition

Instances

Method Endpoint Description
GET /instances/{id} Get instance by ID
GET /instances/{id}/state Get current state with available actions and runtime-resolved field options
GET /instances/{id}/field-options/{fieldName} Resolve dynamic options for a specific field
POST /instances Create new instance
POST /instances/{id}/transition Execute transition
POST /instances/{id}/validate Validate form data
POST /instances/{id}/cancel Cancel instance

Actions

Method Endpoint Description
GET /actions List all registered actions
GET /actions/grouped List actions grouped by category

Custom Route Prefix

app.UseWorkflow("api/v2/workflow"); // Changes prefix

📋 Form Types

Each node can specify how its form should be rendered:

Type Description Use Case
Dynamic Auto-generated from Fields list Simple forms
External Redirect to FormUrl Existing form pages
Component Render named component Custom React components
None No form displayed Auto-transitions, info nodes

External Form Integration

When using External form type, workflow parameters are passed via URL:

/your-form?workflowInstanceId=xxx&workflowNodeId=yyy&workflowActions=approve,reject

Your form can then call the transition API when submitted.


🔀 Condition Expressions

Conditions control which transitions are available.

Supported Operators

Operator Example
==, != status == 'pending'
>, <, >=, <= amount > 10000
AND, OR amount > 10000 AND department == 'IT'
NOT NOT isUrgent

Supported Functions

Function Example
contains() contains(description, 'urgent')
startsWith() startsWith(code, 'PRJ')
endsWith() endsWith(email, '@company.com')
isEmpty() isEmpty(comment)
isNotEmpty() isNotEmpty(approver)

Example

{
  "transitions": [
    {
      "action": "approve",
      "targetNodeId": "gm-approval",
      "condition": "amount > 10000 AND department == 'Finance'"
    },
    {
      "action": "approve", 
      "targetNodeId": "manager-approval",
      "condition": "amount <= 10000"
    }
  ]
}

💾 Database Storage

Chd.Workflow supports multiple database providers out of the box:

Provider Extension Method Description
InMemory AddWorkflow() Default, for development
PostgreSQL AddWorkflowWithPostgreSql() Production-ready with EF Core
SQL Server AddWorkflowWithSqlServer() Production-ready with EF Core

PostgreSQL Setup

// Program.cs
using Chd.Workflow.Extensions;
using Chd.Workflow.Migrations;
using FluentMigrator.Runner;

var builder = WebApplication.CreateBuilder(args);

// Register workflow actions FIRST
builder.Services.AddWorkflowActionsFromCallingAssembly();

// Add workflow with PostgreSQL
var connectionString = builder.Configuration.GetConnectionString("PostgreSQL");
builder.Services.AddWorkflowWithPostgreSql(connectionString!);

// Add FluentMigrator for migrations
builder.Services.AddFluentMigratorCore()
    .ConfigureRunner(rb => rb
        .AddPostgres()
        .WithGlobalConnectionString(connectionString)
        .ScanIn(typeof(M20240419001_CreateWorkflowTables).Assembly).For.Migrations())
    .AddLogging(lb => lb.AddFluentMigratorConsole());

var app = builder.Build();

// Run migrations on startup
using (var scope = app.Services.CreateScope())
{
    var runner = scope.ServiceProvider.GetRequiredService<IMigrationRunner>();
    runner.MigrateUp();
}

app.UseWorkflow();
app.Run();

SQL Server Setup

// Add workflow with SQL Server
var connectionString = builder.Configuration.GetConnectionString("SqlServer");
builder.Services.AddWorkflowWithSqlServer(connectionString!);

// Add FluentMigrator for SQL Server
builder.Services.AddFluentMigratorCore()
    .ConfigureRunner(rb => rb
        .AddSqlServer()
        .WithGlobalConnectionString(connectionString)
        .ScanIn(typeof(M20240419001_CreateWorkflowTables).Assembly).For.Migrations())
    .AddLogging(lb => lb.AddFluentMigratorConsole());

Database Tables

FluentMigrator automatically creates these tables:

workflow_definitions | Column | Type | Description | |--------|------|-------------| | id | varchar(100) | Primary key | | name | varchar(200) | Workflow name | | description | varchar(1000) | Description | | version | int | Version number | | is_active | boolean | Active flag | | root_node_json | text/nvarchar(max) | Serialized node tree | | tenant_id | varchar(100) | Multi-tenant support | | created_at | datetime | Creation timestamp | | updated_at | datetime | Update timestamp | | created_by | varchar(100) | Creator user |

workflow_instances | Column | Type | Description | |--------|------|-------------| | id | varchar(100) | Primary key | | definition_id | varchar(100) | FK to definitions | | definition_version | int | Version at creation | | current_node_id | varchar(100) | Current state | | status | varchar(50) | Draft/Active/Completed/Cancelled | | data_json | text/nvarchar(max) | Instance data | | history_json | text/nvarchar(max) | Transition history | | tenant_id | varchar(100) | Multi-tenant support | | created_at / updated_at / completed_at | datetime | Timestamps | | created_by / updated_by | varchar(100) | User tracking |

Configuration Pattern

// appsettings.json
{
  "DatabaseProvider": "PostgreSQL",  // or "SqlServer" or "InMemory"
  "ConnectionStrings": {
    "PostgreSQL": "Host=localhost;Database=workflow;Username=postgres;Password=secret",
    "SqlServer": "Server=localhost;Database=Workflow;Trusted_Connection=True;"
  }
}
// Program.cs - Provider selection pattern
var dbProvider = builder.Configuration.GetValue<string>("DatabaseProvider") ?? "InMemory";
var connectionString = builder.Configuration.GetConnectionString(dbProvider);

switch (dbProvider.ToLower())
{
    case "postgresql":
        builder.Services.AddWorkflowWithPostgreSql(connectionString!);
        // Add FluentMigrator for PostgreSQL...
        break;
    case "sqlserver":
        builder.Services.AddWorkflowWithSqlServer(connectionString!);
        // Add FluentMigrator for SQL Server...
        break;
    default:
        builder.Services.AddWorkflow(); // InMemory
        break;
}

🔧 Custom Repository

For custom database implementations, implement IWorkflowRepository:

public interface IWorkflowRepository
{
    // Definitions
    Task<WorkflowDefinition?> GetDefinitionAsync(string id, CancellationToken ct = default);
    Task<List<WorkflowDefinition>> GetAllDefinitionsAsync(string? tenantId = null, CancellationToken ct = default);
    Task<WorkflowDefinition> SaveDefinitionAsync(WorkflowDefinition definition, CancellationToken ct = default);
    Task DeleteDefinitionAsync(string id, CancellationToken ct = default);

    // Instances
    Task<WorkflowInstance?> GetInstanceAsync(string id, CancellationToken ct = default);
    Task<WorkflowInstance> SaveInstanceAsync(WorkflowInstance instance, CancellationToken ct = default);
}

Register Custom Repository

builder.Services.AddWorkflow<MyCustomRepository>();

🔍 SQL Query Executor

Load form data and dropdown options from database using SQL queries.

Setup

builder.Services.AddScoped<IWorkflowQueryExecutor, EfCoreQueryExecutor<MyDbContext>>();

Node Data Query

{
  "id": "review-node",
  "dataQuery": "SELECT EmployeeName, Department, ManagerEmail FROM Employees WHERE Id = @employeeId"
}

Field Options Query

{
  "name": "department",
  "type": "Dropdown",
  "optionsQuery": "SELECT Id as value, Name as label FROM Departments WHERE IsActive = 1"
}

Security Features

  • ✅ Parameterized queries (SQL injection protection)
  • ✅ SELECT-only (INSERT/UPDATE/DELETE blocked)
  • ✅ Keyword blacklist (DROP, EXEC, etc.)
  • ✅ Query timeout (30 seconds default)
  • ✅ Row limit (1000 rows default)
  • JsonElement parameter normalization for DB providers
  • ✅ Runtime state sanitization (optionsQuery removed from client payload)

🎨 Frontend Integration

Chd.Workflow pairs with qp-workflow-react for visual workflow design and execution.

npm install qp-workflow-react

Visual Designer

import { TreeDesigner } from 'qp-workflow-react';

function DesignerPage() {
  return (
    <TreeDesigner 
      apiUrl="http://localhost:5035/api/workflow"
      locale="en"  // or "tr" for Turkish
      onSaved={(definition) => console.log('Saved:', definition)}
    />
  );
}

Workflow Runner

import { WorkflowRunner } from 'qp-workflow-react';

function WorkflowPage({ instanceId }) {
  return (
    <WorkflowRunner
      apiUrl="http://localhost:5035/api/workflow"
      instanceId={instanceId}
      locale="en"
      theme="dark"
      onCompleted={() => console.log('Workflow completed!')}
      onTransition={(action) => console.log('Transitioned:', action)}
    />
  );
}

Features

Feature Description
🌳 TreeDesigner Visual tree-based workflow designer
🏃 WorkflowRunner Ready-to-use workflow execution component
🌍 Localization Built-in English and Turkish support
🎨 Theme Support Dark and light themes
📋 Form Editor Design dynamic forms with 15+ field types
Action Binding Select backend methods from dropdown

See the npm package documentation for complete details.


🔄 Migration from Legacy Code

Step 1: Identify Workflow Logic

Find your if-else chains that represent state transitions.

Step 2: Extract Actions

Convert each action block into a IWorkflowAction class:

// Before
if (action == "approve")
{
    request.Status = "Approved";
    SendEmail();
    UpdateBudget();
}

// After
[WorkflowAction("approve-request")]
public class ApproveRequestAction : IWorkflowAction
{
    public async Task<WorkflowActionResult> ExecuteAsync(WorkflowActionContext ctx)
    {
        // Same logic, cleaner structure
        var request = await GetRequest(ctx);
        request.Status = "Approved";
        await SendEmail();
        await UpdateBudget();
        return WorkflowActionResult.Ok();
    }
}

Step 3: Design Workflow

Use TreeDesigner to visually create the workflow and bind actions.

Step 4: Parallel Running

Run both systems side by side during transition:

if (_featureFlags.UseWorkflow("leave-request"))
{
    await _workflowEngine.TransitionAsync(instanceId, action, data);
}
else
{
    // Legacy code
    ProcessRequestLegacy(request, action);
}

Step 5: Remove Legacy Code

Once verified, remove the old if-else chains.


📖 Full Example

Workflow Definition (JSON)

{
  "id": "leave-request",
  "name": "Leave Request Workflow",
  "isActive": true,
  "rootNode": {
    "id": "start",
    "name": "Start",
    "type": "Start",
    "children": [
      {
        "id": "submit",
        "name": "Submit Request",
        "type": "Task",
        "formType": "Dynamic",
        "fields": [
          { "name": "days", "label": "Days", "type": "Number", "required": true },
          { "name": "reason", "label": "Reason", "type": "TextArea", "required": true }
        ],
        "transitions": [
          {
            "action": "submit",
            "label": "Submit",
            "targetNodeId": "review",
            "actionHandler": "send-notification"
          }
        ],
        "children": [
          {
            "id": "review",
            "name": "Manager Review",
            "type": "Task",
            "transitions": [
              {
                "action": "approve",
                "label": "Approve",
                "targetNodeId": "approved",
                "actionHandler": "approve-leave"
              },
              {
                "action": "reject",
                "label": "Reject",
                "targetNodeId": "rejected",
                "actionHandler": "reject-leave",
                "requiresComment": true
              }
            ],
            "children": [
              { "id": "approved", "name": "Approved", "type": "End" },
              { "id": "rejected", "name": "Rejected", "type": "End" }
            ]
          }
        ]
      }
    ]
  }
}

Actions

[WorkflowAction("approve-leave", DisplayName = "Approve Leave", Category = "HR")]
public class ApproveLeaveAction : IWorkflowAction
{
    private readonly ILeaveService _leaveService;
    private readonly IEmailService _emailService;

    public ApproveLeaveAction(ILeaveService leaveService, IEmailService emailService)
    {
        _leaveService = leaveService;
        _emailService = emailService;
    }

    public async Task<WorkflowActionResult> ExecuteAsync(WorkflowActionContext context)
    {
        context.FormData.TryGetValue("leaveId", out var leaveIdObj);
        var leaveId = leaveIdObj?.ToString();

        if (string.IsNullOrEmpty(leaveId))
            return WorkflowActionResult.Fail("Leave ID is required");

        await _leaveService.ApproveAsync(leaveId, context.UserId!);
        await _emailService.SendApprovalNotification(leaveId);

        return WorkflowActionResult.Ok(new Dictionary<string, object?>
        {
            ["approvedAt"] = DateTime.UtcNow,
            ["approvedBy"] = context.UserId
        });
    }
}

[WorkflowAction("reject-leave", DisplayName = "Reject Leave", Category = "HR")]
public class RejectLeaveAction : IWorkflowAction
{
    public Task<WorkflowActionResult> ExecuteAsync(WorkflowActionContext context)
    {
        if (string.IsNullOrEmpty(context.Comment))
            return Task.FromResult(WorkflowActionResult.Fail("Rejection reason is required"));

        // Rejection logic...
        return Task.FromResult(WorkflowActionResult.Ok());
    }
}

API Usage

# Create instance
curl -X POST http://localhost:5035/api/workflow/instances \
  -H "Content-Type: application/json" \
  -d '{"definitionId": "leave-request", "data": {"employeeId": "123"}}'

# Response: { "id": "instance-456", "currentNodeId": "start", ... }

# Submit form
curl -X POST http://localhost:5035/api/workflow/instances/instance-456/transition \
  -H "Content-Type: application/json" \
  -d '{"action": "submit", "data": {"days": 5, "reason": "Vacation"}}'

# Approve (manager)
curl -X POST http://localhost:5035/api/workflow/instances/instance-456/transition \
  -H "Content-Type: application/json" \
  -d '{"action": "approve", "userId": "manager-789"}'

Package Platform Description
Chd.Workflow NuGet .NET Workflow Engine (this package)
qp-workflow-react npm React components for workflow design & execution

📄 License

MIT License - see LICENSE file.


🆕 Recent Updates

  • Query-builder and guard support improved for richer multi-condition editing flows consumed by the designer.
  • Minimal API JSON enum binding hardened to support string enum payloads consistently.
  • Save/update flow compatibility improved for definition persistence scenarios.
  • Added dynamic action input metadata exposure (Inputs, ClrType, required inference) for form auto-generation.
  • Added InputType support on workflow actions and reflection-based input extraction.
  • Added GET /instances/{id}/field-options/{fieldName} for SQL-backed runtime field options.
  • Added SQL query executor integration path for runtime dropdown/radio/multiselect options.
  • Hardened SQL execution path:
    • id/name and value/label fallback mapping
    • blank-label fallback logic
    • JsonElement parameter normalization for DB provider compatibility
  • Runtime payload security hardening:
    • state now returns resolved runtime options
    • raw optionsQuery is sanitized from runtime client payloads
  • Added controller/service method action support via [WorkflowControllerAction] with signature validation and runtime adapter execution.

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.


Made with ❤️ for the .NET community

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.5 105 4/26/2026
1.0.4 96 4/26/2026
1.0.3 97 4/19/2026
1.0.0 100 4/16/2026