Getting Started with WorkflowForge
Install the package, model a short pipeline, run it with a smith and foundry.
Table of Contents
- What’s New in 2.1.1
- What’s New in 2.0.0
- Prerequisites
- Installation
- Your First Workflow
- Core Concepts Explained
- Next Steps
- Troubleshooting
What’s New in 2.1.1
2.1.1 multi-targets .NET 10, .NET 8, and .NET Framework 4.8, removes SupportsRestore in favor of always calling RestoreAsync (base no-op when unused), adds inline restoreAction / restoreFunc, seals event/exception types, hardens threading and disposal, and adds GetOperationOutput / FoundryPropertyKeys. Full notes: CHANGELOG.md.
What’s New in 2.0.0
2.0.0 isolated extension dependencies with ILRepack, shipped Validation and Audit packages, split lifecycle events into three interfaces (see Events Guide), and moved ISystemTimeProvider behind DI. Full notes: CHANGELOG.md.
Prerequisites
You need:
- .NET SDK: .NET 8.0+ or .NET Framework 4.8 (WorkflowForge targets .NET Standard 2.0)
- IDE: Visual Studio 2022, VS Code, or JetBrains Rider
- Basic C#: Comfortable using
async/awaitin console or web apps
Installation
Step 1: Create a project
# Create a new console application
dotnet new console -n MyWorkflowApp
cd MyWorkflowApp
Step 2: Add packages
dotnet add package WorkflowForge
# Optional
dotnet add package WorkflowForge.Extensions.Logging.Serilog
dotnet add package WorkflowForge.Extensions.Resilience.Polly
dotnet add package WorkflowForge.Extensions.Validation
dotnet add package WorkflowForge.Extensions.Audit
Step 3: Build
dotnet build
Your First Workflow
Validate, pay, fulfill.
Step 1: Models
// Models/Order.cs
public class Order
{
public string Id { get; set; } = string.Empty;
public string CustomerId { get; set; } = string.Empty;
public decimal Amount { get; set; }
public string Status { get; set; } = "Pending";
public List<string> Items { get; set; } = new();
}
public class PaymentResult
{
public bool Success { get; set; }
public string TransactionId { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
}
Step 2: Operations
Extend
WorkflowOperationBase, notIWorkflowOperation, unless you have a reason. Use classes when the step will grow or needs tests.
// Operations/ValidateOrderOperation.cs
using WorkflowForge;
using WorkflowForge.Operations;
public class ValidateOrderOperation : WorkflowOperationBase
{
public override string Name => "ValidateOrder";
protected override async Task<object?> ForgeAsyncCore(
object? inputData,
IWorkflowFoundry foundry,
CancellationToken cancellationToken)
{
// Get order from foundry properties (recommended pattern)
var order = foundry.GetPropertyOrDefault<Order>("Order");
if (order == null)
{
throw new InvalidOperationException("Order not found in foundry properties");
}
foundry.Logger.LogInformation("Validating order {OrderId}", order.Id);
// Validation logic
if (string.IsNullOrWhiteSpace(order.CustomerId))
{
throw new ArgumentException("Customer ID is required");
}
if (order.Amount <= 0)
{
throw new ArgumentException("Order amount must be greater than 0");
}
if (order.Items.Count == 0)
{
throw new ArgumentException("Order must contain at least one item");
}
foundry.Logger.LogInformation("Order validation successful");
await Task.CompletedTask;
return null;
}
}
// Operations/ProcessPaymentOperation.cs
using WorkflowForge;
using WorkflowForge.Operations;
public class ProcessPaymentOperation : WorkflowOperationBase
{
public override string Name => "ProcessPayment";
protected override async Task<object?> ForgeAsyncCore(
object? inputData,
IWorkflowFoundry foundry,
CancellationToken cancellationToken)
{
var order = foundry.GetPropertyOrDefault<Order>("Order");
if (order == null)
{
throw new InvalidOperationException("Order not found");
}
foundry.Logger.LogInformation("Processing payment for order {OrderId}", order.Id);
// Simulate payment processing
await Task.Delay(100, cancellationToken); // Simulate API call
var paymentResult = new PaymentResult
{
Success = true,
TransactionId = Guid.NewGuid().ToString("N"),
Message = "Payment processed successfully"
};
// Store result in foundry properties
foundry.SetProperty("PaymentResult", paymentResult);
foundry.Logger.LogInformation("Payment processed: {TransactionId}", paymentResult.TransactionId);
return paymentResult;
}
public override async Task RestoreAsync(
object? outputData,
IWorkflowFoundry foundry,
CancellationToken cancellationToken)
{
// Compensation logic (rollback)
var paymentResult = foundry.GetPropertyOrDefault<PaymentResult>("PaymentResult");
if (paymentResult != null && paymentResult.Success)
{
foundry.Logger.LogWarning("Refunding payment {TransactionId}", paymentResult.TransactionId);
// Simulate refund API call
await Task.Delay(50, cancellationToken);
foundry.Logger.LogInformation("Payment refunded successfully");
}
}
}
// Operations/FulfillOrderOperation.cs
using WorkflowForge;
using WorkflowForge.Operations;
public class FulfillOrderOperation : WorkflowOperationBase
{
public override string Name => "FulfillOrder";
protected override async Task<object?> ForgeAsyncCore(
object? inputData,
IWorkflowFoundry foundry,
CancellationToken cancellationToken)
{
var order = foundry.GetPropertyOrDefault<Order>("Order");
if (order == null)
{
throw new InvalidOperationException("Order not found");
}
foundry.Logger.LogInformation("Fulfilling order {OrderId}", order.Id);
// Simulate fulfillment
await Task.Delay(50, cancellationToken);
order.Status = "Fulfilled";
foundry.Logger.LogInformation("Order fulfilled successfully");
return order;
}
}
Step 3: Wire and run
// Program.cs
using WorkflowForge;
using WorkflowForge.Loggers;
// Create an order
var order = new Order
{
Id = Guid.NewGuid().ToString("N"),
CustomerId = "CUST-123",
Amount = 99.99m,
Items = new List<string> { "Product A", "Product B" }
};
Console.WriteLine($"Processing order {order.Id}...\n");
// Build the workflow
var workflow = WorkflowForge.CreateWorkflow("ProcessOrder")
.WithDescription("Complete order processing workflow")
.WithVersion("2.1.1")
.AddOperation(new ValidateOrderOperation())
.AddOperation(new ProcessPaymentOperation())
.AddOperation(new FulfillOrderOperation())
.Build();
// Create a foundry with the order data
var foundry = WorkflowForge.CreateFoundry(
"ProcessOrder",
initialProperties: new Dictionary<string, object?> { ["Order"] = order }
);
// Create a smith (orchestrator) with console logger
using var smith = WorkflowForge.CreateSmith(new ConsoleLogger());
try
{
// Execute the workflow
await smith.ForgeAsync(workflow, foundry);
Console.WriteLine($"\nOrder processed successfully!");
Console.WriteLine($"Final status: {order.Status}");
var paymentResult = foundry.GetPropertyOrDefault<PaymentResult>("PaymentResult");
if (paymentResult != null)
{
Console.WriteLine($"Transaction ID: {paymentResult.TransactionId}");
}
}
catch (Exception ex)
{
Console.WriteLine($"\nWorkflow failed: {ex.Message}");
}
Step 4: Run
dotnet run
Sample console output:
Processing order abc123...
[INFO] Validating order abc123
[INFO] Order validation successful
[INFO] Processing payment for order abc123
[INFO] Payment processed: def456
[INFO] Fulfilling order abc123
[INFO] Order fulfilled successfully
Order processed successfully!
Final status: Fulfilled
Transaction ID: def456
Core Concepts Explained
The metaphor
Terms map to orchestration: forge builds graphs; foundry holds state; smith runs steps; each operation is one tool.
Pieces:
- Forge: Main factory for creating workflows
- Foundry: Execution environment with shared data
- Smith: Orchestrator that executes workflows
- Operation: Individual executable task
More detail: Architecture Guide.
Data flow
Primary: foundry.Properties
// Store data
foundry.SetProperty("Key", value);
// Retrieve data
var value = foundry.GetPropertyOrDefault<T>("Key");
// Retrieve with default
var value = foundry.GetPropertyOrDefault<T>("Key", defaultValue);
Optional: WorkflowOperationBase<TInput, TOutput> for fixed contracts
public class MyOperation : WorkflowOperationBase<Order, OrderResult>
{
public override string Name => "MyOperation";
protected override async Task<OrderResult> ForgeAsyncCore(
Order input,
IWorkflowFoundry foundry,
CancellationToken cancellationToken)
{
// Typed input and output
return new OrderResult { Success = true };
}
}
Prefer the bag for most flows. Typed ops help when the contract is stable.
Compensation
Implement RestoreAsync where undo matters; the base no-op is skipped safely on failure. Inline ops can pass restoreAction:
var workflow = WorkflowForge.CreateWorkflow("OrderWorkflow")
.AddOperation("ProcessPayment", async (foundry, ct) =>
{
foundry.Properties["payment_id"] = "PAY-123";
// Process payment...
},
restoreAction: async (foundry, ct) =>
{
var paymentId = foundry.Properties["payment_id"];
// Refund payment...
})
.Build();
Class-based steps: override RestoreAsync.
public class MyOperation : WorkflowOperationBase
{
protected override async Task<object?> ForgeAsyncCore(...)
{
// Forward logic
}
public override async Task RestoreAsync(...)
{
// Compensation/rollback logic
}
}
Behavior flags:
var options = new WorkflowForgeOptions
{
Enabled = true,
ContinueOnError = false,
FailFastCompensation = false,
ThrowOnCompensationError = false,
EnableOutputChaining = true
};
var foundry = WorkflowForge.CreateFoundry("MyWorkflow", options: options);
Next Steps
Samples
Repo has 33 samples:
# Clone the repository
git clone https://github.com/animatlabs/workflow-forge.git
cd workflow-forge
# Run the samples
cd src/samples/WorkflowForge.Samples.BasicConsole
dotnet run
Parallel steps
AddParallelOperations:
var workflow = WorkflowForge.CreateWorkflow("ParallelProcessing")
.AddParallelOperations(
new ValidateInventoryOperation(),
new CheckFraudOperation(),
new VerifyCustomerOperation()
)
.AddOperation(new ProcessOrderOperation())
.Build();
// With concurrency control
var workflow2 = WorkflowForge.CreateWorkflow("ControlledParallel")
.AddParallelOperations(
operations,
maxConcurrency: 4,
timeout: TimeSpan.FromSeconds(30),
name: "ParallelValidation"
)
.Build();
Tests
WorkflowForge.Testing + FakeWorkflowFoundry:
// Install testing package
dotnet add package WorkflowForge.Testing
using WorkflowForge.Testing;
using Xunit;
public class MyOperationTests
{
[Fact]
public async Task Operation_Should_SetProperty()
{
// Arrange
var foundry = new FakeWorkflowFoundry();
var operation = new ValidateOrderOperation();
foundry.Properties["Order"] = new Order { Id = "123", Amount = 99.99m };
// Act
await operation.ForgeAsync(null, foundry, CancellationToken.None);
// Assert - no exception means validation passed
}
}
Extensions
Serilog:
using WorkflowForge.Extensions.Logging.Serilog;
var logger = SerilogLoggerFactory.CreateLogger(new SerilogLoggerOptions
{
MinimumLevel = "Information",
EnableConsoleSink = true
});
var smith = WorkflowForge.CreateSmith(logger);
Polly:
using WorkflowForge.Extensions.Resilience.Polly;
// Retry, breaker, timeout together
foundry.UsePollyComprehensive(
maxRetryAttempts: 3,
circuitBreakerThreshold: 5,
circuitBreakerDuration: TimeSpan.FromSeconds(30));
// Or one policy at a time
foundry.UsePollyRetry(maxRetryAttempts: 3);
foundry.UsePollyCircuitBreaker(failureThreshold: 5, durationOfBreak: TimeSpan.FromSeconds(30));
Validation:
using WorkflowForge.Extensions.Validation;
foundry.UseValidation(
f => f.GetPropertyOrDefault<Order>("Order"));
Read next
- Architecture Overview - Layout and patterns
- Operations Guide - Built-in and custom operations
- Event System - Lifecycle events and monitoring
- Configuration - Environment-specific settings
- API Reference - Member-level reference
Troubleshooting
Common issues
“Workflow name is required”
Use .WithName() or CreateWorkflow(name).
Nothing runs
Call .Build() and await smith.ForgeAsync(...).
Data missing between steps
Write with foundry.SetProperty, read with foundry.GetPropertyOrDefault.
Compensation never does anything
Override RestoreAsync where rollback is real; on failure the smith walks completed ops in reverse and skips base no-ops.
Getting Help
- GitHub Issues: https://github.com/animatlabs/workflow-forge/issues
- Documentation: https://github.com/animatlabs/workflow-forge/tree/main/docs
- Samples: https://github.com/animatlabs/workflow-forge/tree/main/src/samples
Summary
You added packages, three WorkflowOperationBase steps, a smith, a foundry, and saw properties and RestoreAsync.
Next: Samples catalog, project WorkflowForge.Samples.BasicConsole.
Related Documentation
- Architecture Overview - Metaphor and layout
- Operations Guide - Built-in ops and patterns
- Events System - Lifecycle hooks
- Extensions - Optional packages
- Configuration - Options and appsettings
- API Reference - Member listings