| name | conductor-sharp |
| description | Comprehensive guide for using ConductorSharp library to build Conductor workflows in .NET. Use when creating task handlers, workflow definitions, configuring execution engines, scaffolding definitions, or integrating ConductorSharp into .NET projects. Covers all task types, client services, patterns package, and toolkit usage. |
ConductorSharp Library Guide
Complete guide for building Conductor workflows using ConductorSharp's strongly-typed DSL, task handlers, and execution engine.
Quick Reference
Packages
ConductorSharp.Client - API client
ConductorSharp.Engine - Workflow engine, builder DSL, handlers
ConductorSharp.Patterns - Built-in tasks (WaitSeconds, ReadWorkflowTasks, C# Lambda, Signal Wait)
ConductorSharp.KafkaCancellationNotifier - Kafka cancellation support
ConductorSharp.Toolkit - CLI scaffolding tool
Project Setup
Adding to Existing Project
dotnet add package ConductorSharp.Client
dotnet add package ConductorSharp.Engine
Creating New Console Project
dotnet new console -n MyConductorApp
cd MyConductorApp
dotnet add package ConductorSharp.Client
dotnet add package ConductorSharp.Engine
Basic Configuration
using ConductorSharp.Engine.Extensions;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
builder.Services
.AddConductorSharp(baseUrl: "http://localhost:8080")
.AddExecutionManager(
maxConcurrentWorkers: 10,
sleepInterval: 500,
longPollInterval: 100,
domain: null,
typeof(Program).Assembly
);
builder.Services.RegisterWorkflow<MyWorkflow>();
var host = builder.Build();
await host.RunAsync();
Writing Task Handlers
using ConductorSharp.Engine.Builders.Metadata;
using ConductorSharp.Engine;
using ConductorSharp.Engine.Util;
[OriginalName("MY_TASK_name")]
public class MyTaskHandler : TaskRequestHandler<MyTaskRequest, MyTaskResponse>
{
private readonly ConductorSharpExecutionContext _context;
public MyTaskHandler(ConductorSharpExecutionContext context)
{
_context = context;
}
public override async Task<MyTaskResponse> Handle(MyTaskRequest request, CancellationToken cancellationToken)
{
return new MyTaskResponse { };
}
}
Request/Response Models
public class MyTaskRequest : IRequest<MyTaskResponse>
{
[Required]
public string InputValue { get; set; }
}
public class MyTaskResponse
{
public string OutputValue { get; set; }
}
Registering Standalone Tasks
services.RegisterWorkerTask<MyTaskHandler>(options =>
{
options.OwnerEmail = "team@example.com";
options.Description = "My task description";
});
Writing Workflow Definitions
Basic Structure
using ConductorSharp.Engine.Builders;
using ConductorSharp.Engine.Builders.Metadata;
public class MyWorkflowInput : WorkflowInput<MyWorkflowOutput>
{
public int CustomerId { get; set; }
}
public class MyWorkflowOutput : WorkflowOutput
{
public string Result { get; set; }
}
[OriginalName("MY_workflow")]
[WorkflowMetadata(OwnerEmail = "team@example.com")]
public class MyWorkflow : Workflow<MyWorkflow, MyWorkflowInput, MyWorkflowOutput>
{
public MyWorkflow(WorkflowDefinitionBuilder<MyWorkflow, MyWorkflowInput, MyWorkflowOutput> builder)
: base(builder) { }
public SomeTaskHandler FirstTask { get; set; }
public AnotherTaskHandler SecondTask { get; set; }
public override void BuildDefinition()
{
_builder.AddTask(wf => wf.FirstTask, wf => new SomeTaskRequest { Input = wf.WorkflowInput.CustomerId });
_builder.AddTask(wf => wf.SecondTask, wf => new AnotherTaskRequest { Input = wf.FirstTask.Output.Result });
_builder.SetOutput(wf => new MyWorkflowOutput { Result = wf.SecondTask.Output.Value });
}
}
Workflow Metadata
[WorkflowMetadata(
OwnerEmail = "team@example.com",
OwnerApp = "my-app",
Description = "Workflow description",
FailureWorkflow = typeof(FailureHandlerWorkflow)
)]
Versioning
[Version(2)]
public class MyWorkflow : Workflow<...> { }
Task Types
Simple Task
public MyTaskHandler MyTask { get; set; }
_builder.AddTask(wf => wf.MyTask, wf => new MyTaskRequest { InputValue = wf.WorkflowInput.Value });
Sub-Workflow Task
Sub-workflows allow referencing other workflows as tasks. Define a model class that inherits from SubWorkflowTaskModel:
[OriginalName("CHILD_workflow")]
public class ChildWorkflow : SubWorkflowTaskModel<ChildWorkflowInput, ChildWorkflowOutput> { }
public ChildWorkflow ChildWorkflow { get; set; }
_builder.AddTask(wf => wf.ChildWorkflow, wf => new ChildWorkflowInput { CustomerId = wf.WorkflowInput.CustomerId });
Switch Task (Conditional Branching)
The Switch task evaluates a case value and executes tasks in the matching branch:
public SwitchTaskModel SwitchTask { get; set; }
public CustomerGetHandler GetCustomer { get; set; }
public TerminateTaskModel Terminate { get; set; }
_builder.AddTask(
wf => wf.SwitchTask,
wf => new SwitchTaskInput { SwitchCaseValue = wf.WorkflowInput.Operation },
new DecisionCases<MyWorkflow>
{
["process"] = builder => builder.AddTask(wf => wf.GetCustomer, wf => new CustomerGetRequest { CustomerId = 1 }),
["skip"] = builder => { },
DefaultCase = builder => builder.AddTask(wf => wf.Terminate, wf => new TerminateTaskInput { TerminationStatus = TerminationStatus.Failed })
}
);
Decision Task (Deprecated - Use Switch)
#pragma warning disable CS0618
public DecisionTaskModel Decision { get; set; }
public CustomerGetHandler GetCustomer { get; set; }
public TerminateTaskModel Terminate { get; set; }
_builder.AddTask(
wf => wf.Decision,
wf => new DecisionTaskInput { CaseValueParam = "test" },
new DecisionCases<MyWorkflow>
{
["test"] = builder => builder.AddTask(wf => wf.GetCustomer, wf => new CustomerGetRequest { CustomerId = 1 }),
DefaultCase = builder => builder.AddTask(wf => wf.Terminate, wf => new TerminateTaskInput { TerminationStatus = TerminationStatus.Failed })
}
);
#pragma warning restore CS0618
Dynamic Task
Dynamic tasks allow selecting which task to execute at runtime. The task name is determined by a workflow input or computed value. You define the expected input/output types that the dynamically selected task should conform to:
public class ExpectedDynamicInput : IRequest<ExpectedDynamicOutput>
{
public int CustomerId { get; set; }
}
public class ExpectedDynamicOutput
{
public string Name { get; set; }
public string Address { get; set; }
}
public class MyWorkflowInput : WorkflowInput<MyWorkflowOutput>
{
public string TaskName { get; set; }
public int CustomerId { get; set; }
}
public DynamicTaskModel<ExpectedDynamicInput, ExpectedDynamicOutput> DynamicHandler { get; set; }
_builder.AddTask(
wf => wf.DynamicHandler,
wf => new DynamicTaskInput<ExpectedDynamicInput, ExpectedDynamicOutput>
{
TaskInput = new ExpectedDynamicInput { CustomerId = wf.WorkflowInput.CustomerId },
TaskToExecute = wf.WorkflowInput.TaskName
}
);
_builder.AddTask(
wf => wf.PrepareEmail,
wf => new PrepareEmailRequest { Name = wf.DynamicHandler.Output.Name, Address = wf.DynamicHandler.Output.Address }
);
Dynamic Fork-Join Task
public DynamicForkJoinTaskModel DynamicFork { get; set; }
_builder.AddTask(
wf => wf.DynamicFork,
wf => new DynamicForkJoinInput
{
DynamicTasks = ,
DynamicTasksInput =
}
);
Do-While Loop Task
The Do-While task executes a set of tasks repeatedly while a condition is true. The loop condition uses JSONPath expressions where:
$.do_while.iteration - the current iteration number (0-based)
$.value - the value passed in the DoWhileInput.Value property
public class MyWorkflowInput : WorkflowInput<MyWorkflowOutput>
{
public int Loops { get; set; }
}
public DoWhileTaskModel DoWhile { get; set; }
public CustomerGetHandler GetCustomer { get; set; }
_builder.AddTask(
wf => wf.DoWhile,
wf => new DoWhileInput { Value = wf.WorkflowInput.Loops },
"$.do_while.iteration < $.value",
builder =>
{
builder.AddTask(wf => wf.GetCustomer, wf => new CustomerGetRequest { CustomerId = "CUSTOMER-1" });
}
);
The loop continues as long as the condition evaluates to true. In this example, if Loops = 3, the inner tasks execute 3 times (iterations 0, 1, 2).
Note: ConductorSharp does not provide a strongly typed output for the DoWhile task, as can be seen from the implementation:
public class DoWhileTaskModel : TaskModel<DoWhileInput, NoOutput>
{
}
Lambda Task (JavaScript)
The Lambda task executes inline JavaScript code. Define input/output models:
public class LambdaInput : IRequest<LambdaOutput>
{
public string Value { get; set; }
}
public class LambdaOutput
{
public string Something { get; set; }
}
public LambdaTaskModel<LambdaInput, LambdaOutput> LambdaTask { get; set; }
_builder.AddTask(
wf => wf.LambdaTask,
wf => new LambdaInput { Value = wf.WorkflowInput.Input },
script: "return { something: $.Value.toUpperCase() }"
);
For context, in the above parameterized generic class LambdaTaskModel, the LambdaOutput instance is available as Output.Result.Something. This is less than ideal, but is the current way of things. Reasoning can be seen in the implementation:
public abstract class LambdaOutputModel<O>
{
public O Result { get; set; }
}
public abstract class LambdaTaskModel<I, O> where I : IRequest<O>
{
public I Input { get; set; }
public LambdaOutputModel<O> Output { get; set; }
}```
### C# Lambda Task (Patterns Package)
The C# Lambda task executes inline C# code. Requires the Patterns package.
```csharp
public class LambdaInput : IRequest<LambdaOutput>
{
public string Value { get; set; }
}
public class LambdaOutput
{
public string Result { get; set; }
}
public CSharpLambdaTaskModel<LambdaInput, LambdaOutput> InlineLambda { get; set; }
_builder.AddTask(
wf => wf.InlineLambda,
wf => new LambdaInput { Value = wf.WorkflowInput.Input },
input => new LambdaOutput { Result = input.Value.ToUpperInvariant() }
);
Wait Task
The Wait task pauses workflow execution for a duration or until a specific time:
public WaitTaskModel WaitTask { get; set; }
_builder.AddTask(
wf => wf.WaitTask,
wf => new WaitTaskInput { Duration = "1s" }
);
_builder.AddTask(
wf => wf.WaitTask,
wf => new WaitTaskInput { Until = "2024-12-31 11:59" }
);
WaitSeconds Task (Patterns Package)
A convenience task for waiting a specific number of seconds:
public WaitSeconds WaitTask { get; set; }
_builder.AddTask(wf => wf.WaitTask, wf => new WaitSecondsRequest { Seconds = 30 });
Signal Wait (Patterns Package)
The Signal Wait pattern allows workflows to pause and wait for an external signal before continuing. This is useful for scenarios like:
- Waiting for external system callbacks
- Human approval workflows
- Coordinating between multiple workflows
- Integrating with external event sources
Architecture
The Signal Wait pattern consists of several components:
| Component | Description |
|---|
SignalWait | A subworkflow that pauses execution until signaled |
RegisterWaiter | Task that registers the waiting workflow in the signal store |
ISignalStore | Persistence abstraction for signal entries (implement your own for production) |
ISignalService | Service to send signals and unblock waiting workflows |
SignalSweeperService | Background service that completes WAIT tasks when signals arrive |
InMemorySignalStore | Development-only in-memory implementation |
Setup
services
.AddConductorSharp(baseUrl: "http://localhost:8080")
.AddExecutionManager(...)
.AddSignalWait<YourSignalStore>("OPTIONAL_PREFIX");
services.RegisterWorkflow<SignalWait>();
Important: The InMemorySignalStore is only suitable for development/testing. For production, implement ISignalStore with a persistent backend (database, Redis, etc.) to ensure signals survive process restarts and work across multiple instances.
Using Signal Wait in a Workflow
using ConductorSharp.Patterns.Workflows;
public class MyWorkflowInput : WorkflowInput<MyWorkflowOutput>
{
public string OrderId { get; set; }
}
public class MyWorkflow : Workflow<MyWorkflow, MyWorkflowInput, MyWorkflowOutput>
{
public ProcessOrderHandler ProcessOrder { get; set; }
public SignalWait WaitForPayment { get; set; }
public CompleteOrderHandler CompleteOrder { get; set; }
public override void BuildDefinition()
{
_builder.AddTask(wf => wf.ProcessOrder, wf => new ProcessOrderRequest { OrderId = wf.WorkflowInput.OrderId });
_builder.AddTask(wf => wf.WaitForPayment, wf => new SignalWaitInput
{
SignalKey = $"payment_{wf.WorkflowInput.OrderId}"
});
_builder.AddTask(wf => wf.CompleteOrder, wf => new CompleteOrderRequest
{
OrderId = wf.WorkflowInput.OrderId,
PaymentStatus = wf.WaitForPayment.Output.SignalStatus
});
}
}
Sending a Signal
Use ISignalService to send signals from your API or other services:
public class PaymentController : ControllerBase
{
private readonly ISignalService _signalService;
public PaymentController(ISignalService signalService)
{
_signalService = signalService;
}
[HttpPost("payment-confirmed/{orderId}")]
public async Task<IActionResult> PaymentConfirmed(string orderId, [FromBody] PaymentResult result)
{
await _signalService.SendAsync($"payment_{orderId}", result.Status);
return Ok();
}
}
Signal Key Design
Signal keys should be unique and predictable:
- Use business identifiers:
$"order_{orderId}", $"approval_{requestId}"
- Include workflow context when needed:
$"{workflowType}_{entityId}"
- Avoid collisions by including unique prefixes
Order Independence
The signal pattern is order-independent:
- Signal arrives first: Stored until a workflow registers to wait for it
- Workflow waits first: Waits until a signal arrives with matching key
This ensures reliable coordination regardless of timing.
Task Configuration
The RegisterWaiter task is configured with specific settings for reliability:
- ConcurrentExecLimit = 1: Only one registration executes at a time per worker, preventing race conditions
- RetryCount = 10 with RetryDelaySeconds = 1: Provides resilience against transient failures
These settings serialize registrations, which may introduce slight delays under high load but ensures consistency.
Implementing ISignalStore for Production
public class DatabaseSignalStore : ISignalStore
{
private readonly IDbConnection _db;
public async Task<SignalEntry?> GetAsync(string signalKey, CancellationToken ct = default)
{
return await _db.QueryFirstOrDefaultAsync<SignalEntry>(
"SELECT * FROM SignalEntries WHERE SignalKey = @signalKey",
new { signalKey });
}
public async Task RegisterWaiterAsync(string signalKey, string waitWorkflowId, string waitTaskRefName, CancellationToken ct = default)
{
await _db.ExecuteAsync(
@"INSERT INTO SignalEntries (SignalKey, WaitWorkflowId, WaitTaskRefName, CreatedAt)
VALUES (@signalKey, @waitWorkflowId, @waitTaskRefName, @createdAt)
ON CONFLICT (SignalKey) DO UPDATE SET WaitWorkflowId = @waitWorkflowId, WaitTaskRefName = @waitTaskRefName",
new { signalKey, waitWorkflowId, waitTaskRefName, createdAt = DateTime.UtcNow });
}
public async Task RegisterSignalAsync(string signalKey, string signalStatus, CancellationToken ct = default)
{
await _db.ExecuteAsync(
@"INSERT INTO SignalEntries (SignalKey, SignalStatus, CreatedAt)
VALUES (@signalKey, @signalStatus, @createdAt)
ON CONFLICT (SignalKey) DO UPDATE SET SignalStatus = @signalStatus",
new { signalKey, signalStatus, createdAt = DateTime.UtcNow });
}
public async Task DeleteAsync(string signalKey, CancellationToken ct = default)
{
await _db.ExecuteAsync("DELETE FROM SignalEntries WHERE SignalKey = @signalKey", new { signalKey });
}
public async Task<IReadOnlyList<SignalEntry>> GetPendingWaitersAsync(CancellationToken ct = default)
{
return (await _db.QueryAsync<SignalEntry>(
"SELECT * FROM SignalEntries WHERE WaitWorkflowId IS NOT NULL AND SignalStatus IS NULL"))
.ToList();
}
}
Terminate Task
The Terminate task ends the workflow execution with a specific status and output:
public TerminateTaskModel TerminateTask { get; set; }
_builder.AddTask(
wf => wf.TerminateTask,
wf => new TerminateTaskInput
{
TerminationStatus = TerminationStatus.Completed,
WorkflowOutput = new { Property = "Test", Result = "Done" }
}
);
Human Task
The Human task pauses the workflow until a human completes an action (e.g., approval):
public class HumanTaskOutput
{
public string CustomerId { get; set; }
public bool Approved { get; set; }
}
public HumanTaskModel<HumanTaskOutput> HumanTask { get; set; }
public CustomerGetHandler GetCustomer { get; set; }
_builder.AddTask(wf => wf.HumanTask, wf => new HumanTaskInput<HumanTaskOutput> { });
_builder.AddTask(wf => wf.GetCustomer, wf => new CustomerGetRequest { CustomerId = wf.HumanTask.Output.CustomerId });
JSON JQ Transform Task
The JSON JQ Transform task applies JQ expressions to transform data:
public class JqInput : IRequest<JqOutput>
{
public string QueryExpression { get; set; }
public object Data { get; set; }
}
public class JqOutput
{
public object Result { get; set; }
}
public JsonJqTransformTaskModel<JqInput, JqOutput> TransformTask { get; set; }
_builder.AddTask(
wf => wf.TransformTask,
wf => new JqInput
{
QueryExpression = ".data | map(.name)",
Data = wf.WorkflowInput.Items
}
);
ReadWorkflowTasks Task (Patterns Package)
Reads task data from another workflow execution:
public ReadWorkflowTasks ReadTasks { get; set; }
_builder.AddTask(
wf => wf.ReadTasks,
wf => new ReadWorkflowTasksInput
{
WorkflowId = wf.WorkflowInput.TargetWorkflowId,
TaskNames = "task1,task2"
}
);
Optional Tasks
Mark a task as optional so workflow continues even if the task fails:
_builder.AddTask(wf => wf.OptionalTask, wf => new OptionalTaskRequest { Value = "test" }).AsOptional();
PassThrough Task (Raw Definition)
For unsupported task types:
_builder.AddTasks(new WorkflowTask
{
Name = "CUSTOM_task",
TaskReferenceName = "custom_ref",
Type = "CUSTOM",
InputParameters = new Dictionary<string, object> { ["key"] = "value" }
});
Configuration
Execution Manager
services
.AddConductorSharp(baseUrl: "http://localhost:8080")
.AddExecutionManager(
maxConcurrentWorkers: 10,
sleepInterval: 500,
longPollInterval: 100,
domain: "my-domain",
typeof(Program).Assembly
);
Multiple Conductor Instances
services
.AddConductorSharp(baseUrl: "http://primary-conductor:8080")
.AddAlternateClient(
baseUrl: "http://secondary-conductor:8080",
key: "Secondary",
apiPath: "api",
ignoreInvalidCertificate: false
);
public class MyController(
IWorkflowService primaryService,
[FromKeyedServices("Secondary")] IWorkflowService secondaryService
) { }
Poll Timing Strategies
.AddExecutionManager(...)
.AddExecutionManager(...)
.UseConstantPollTimingStrategy()
Beta Execution Manager
.AddExecutionManager(...)
.UseBetaExecutionManager()
Patterns Package
.AddExecutionManager(...)
.AddConductorSharpPatterns()
.AddCSharpLambdaTasks()
.AddSignalWait<YourSignalStore>()
Kafka Cancellation Notifier
.AddExecutionManager(...)
.AddKafkaCancellationNotifier(
kafkaBootstrapServers: "localhost:9092",
topicName: "conductor.status.task",
groupId: "my-worker-group",
createTopicOnStartup: true
)
Pipeline Behaviors
.AddPipelines(pipelines =>
{
pipelines.AddCustomBehavior(typeof(MyCustomBehavior<,>));
pipelines.AddExecutionTaskTracking();
pipelines.AddContextLogging();
pipelines.AddRequestResponseLogging();
pipelines.AddValidation();
})
Custom Behavior
public class TimingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
var sw = Stopwatch.StartNew();
var response = await next();
Console.WriteLine($"Execution took {sw.ElapsedMilliseconds}ms");
return response;
}
}
Health Checks
builder.Services.AddHealthChecks()
.AddCheck<ConductorSharpHealthCheck>("conductor-worker");
.AddExecutionManager(...)
.SetHealthCheckService<FileHealthService>()
Client Services
Available services for direct Conductor API access:
IWorkflowService - Start, pause, resume, terminate workflows
ITaskService - Update tasks, get logs, poll for tasks
IMetadataService - Manage workflow/task definitions
IAdminService - Admin operations, queue management
IEventService - Event handlers
IQueueAdminService - Queue administration
IWorkflowBulkService - Bulk workflow operations
IHealthService - Conductor server health
IExternalPayloadService - External payload storage
Example Usage
public class WorkflowController : ControllerBase
{
private readonly IWorkflowService _workflowService;
private readonly IMetadataService _metadataService;
public WorkflowController(IWorkflowService workflowService, IMetadataService metadataService)
{
_workflowService = workflowService;
_metadataService = metadataService;
}
[HttpPost("start")]
public async Task<string> StartWorkflow([FromBody] StartRequest request)
{
return await _workflowService.StartAsync(new StartWorkflowRequest
{
Name = "MY_workflow",
Version = 1,
Input = new Dictionary<string, object> { ["customerId"] = request.CustomerId }
});
}
[HttpGet("definitions")]
public async Task<ICollection<WorkflowDef>> GetDefinitions()
{
return await _metadataService.ListWorkflowsAsync();
}
}
Scaffolding with Toolkit
Installation
dotnet tool install --global ConductorSharp.Toolkit --version 4.0.1
Configuration
Create conductorsharp.yaml:
baseUrl: http://localhost:8080
apiPath: api
namespace: MyApp.Generated
destination: ./Generated
Usage
dotnet-conductorsharp
dotnet-conductorsharp -f myconfig.yaml
dotnet-conductorsharp -n CUSTOMER_get -n ORDER_create
dotnet-conductorsharp -e team@example.com
dotnet-conductorsharp -a my-application
dotnet-conductorsharp --no-tasks
dotnet-conductorsharp --no-workflows
dotnet-conductorsharp --dry-run
Execution Context
Access workflow/task metadata in handlers:
public class MyHandler : TaskRequestHandler<MyRequest, MyResponse>
{
private readonly ConductorSharpExecutionContext _context;
public MyHandler(ConductorSharpExecutionContext context)
{
_context = context;
}
public override async Task<MyResponse> Handle(MyRequest request, CancellationToken cancellationToken)
{
var workflowId = _context.WorkflowId;
var taskId = _context.TaskId;
var correlationId = _context.CorrelationId;
}
}
Task Domain Assignment
[TaskDomain("my-domain")]
public class MyTaskHandler : TaskRequestHandler<...> { }
Common Patterns
Workflow with Multiple Tasks
public GetCustomerHandler GetCustomer { get; set; }
public PrepareEmailHandler PrepareEmail { get; set; }
public override void BuildDefinition()
{
_builder.AddTask(wf => wf.GetCustomer, wf => new GetCustomerRequest { CustomerId = wf.WorkflowInput.CustomerId });
_builder.AddTask(wf => wf.PrepareEmail, wf => new PrepareEmailRequest
{
Name = wf.GetCustomer.Output.Name,
Address = wf.GetCustomer.Output.Address
});
_builder.SetOutput(wf => new MyWorkflowOutput { EmailBody = wf.PrepareEmail.Output.EmailBody });
}
Conditional Workflow
public SwitchTaskModel SwitchTask { get; set; }
public ProcessTaskHandler ProcessTask { get; set; }
public DefaultTaskHandler DefaultTask { get; set; }
_builder.AddTask(
wf => wf.SwitchTask,
wf => new SwitchTaskInput { SwitchCaseValue = wf.WorkflowInput.Operation },
new DecisionCases<MyWorkflow>
{
["process"] = builder => builder.AddTask(wf => wf.ProcessTask, wf => new ProcessTaskRequest { Value = "data" }),
["skip"] = builder => { },
DefaultCase = builder => builder.AddTask(wf => wf.DefaultTask, wf => new DefaultTaskRequest { })
}
);
Error Handling with Failure Workflow
[WorkflowMetadata(FailureWorkflow = typeof(HandleFailureWorkflow))]
public class MyWorkflow : Workflow<...> { }
Best Practices
- Use
[OriginalName] attribute for custom task/workflow names in Conductor
- Register workflows with
services.RegisterWorkflow<MyWorkflow>()
- Use strongly-typed models for inputs/outputs instead of dictionaries
- Add validation using DataAnnotations and
.AddValidation() pipeline
- Use patterns package for common tasks (WaitSeconds, ReadWorkflowTasks, C# Lambda, Signal Wait)
- Configure health checks for production deployments
- Use scaffolding tool to generate models from existing Conductor definitions
- Implement persistent ISignalStore for production Signal Wait usage (not InMemorySignalStore)