conductor-sharp
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.
SKILL.md
| 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. |
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 clientConductorSharp.Engine- Workflow engine, builder DSL, handlersConductorSharp.Patterns- Built-in tasks (WaitSeconds, ReadWorkflowTasks, C# Lambda, Signal Wait)ConductorSharp.KafkaCancellationNotifier- Kafka cancellation supportConductorSharp.Toolkit- CLI scaffolding tool
Project Setup
Adding to Existing Project
// Install packages
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; // Access workflow/task metadata
}
public override async Task<MyTaskResponse> Handle(MyTaskRequest request, CancellationToken cancellationToken)
{
// Access context: _context.WorkflowId, _context.TaskId, _context.CorrelationId
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) { }
// Task properties
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)] // Version number for sub-workflow references
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:
// Define the sub-workflow model (usually scaffolded or defined separately)
[OriginalName("CHILD_workflow")]
public class ChildWorkflow : SubWorkflowTaskModel<ChildWorkflowInput, ChildWorkflowOutput> { }
// In the parent workflow:
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 => { /* skip processing - no tasks */ },
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:
// Define the expected input/output for the dynamic task
public class ExpectedDynamicInput : IRequest<ExpectedDynamicOutput>
{
public int CustomerId { get; set; }
}
public class ExpectedDynamicOutput
{
public string Name { get; set; }
public string Address { get; set; }
}
// In the workflow:
public class MyWorkflowInput : WorkflowInput<MyWorkflowOutput>
{
public string TaskName { get; set; } // e.g., "CUSTOMER_get_v1" or "CUSTOMER_get_v2"
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 // Task name resolved at runtime
}
);
// Access the output after the dynamic task executes
_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 = /* list of task names */,
DynamicTasksInput = /* corresponding inputs */
}
);
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 theDoWhileInput.Valueproperty
public class MyWorkflowInput : WorkflowInput<MyWorkflowOutput>
{
public int Loops { get; set; } // Number of iterations
}
public DoWhileTaskModel DoWhile { get; set; }
public CustomerGetHandler GetCustomer { get; set; }
_builder.AddTask(
wf => wf.DoWhile,
wf => new DoWhileInput { Value = wf.WorkflowInput.Loops }, // Value used in condition
"$.do_while.iteration < $.value", // Loop while iteration < Loops
builder =>
{
// Tasks to execute in each iteration
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() }" // JavaScript expression
);
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
// Requires: .AddCSharpLambdaTasks()
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() } // C# lambda expression
);
Wait Task
The Wait task pauses workflow execution for a duration or until a specific time:
public WaitTaskModel WaitTask { get; set; }
// Wait for a duration (supports: s, m, h, d for seconds, minutes, hours, days)
_builder.AddTask(
wf => wf.WaitTask,
wf => new WaitTaskInput { Duration = "1s" }
);
// Or wait until a specific datetime
_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:
// Requires: .AddConductorSharpPatterns()
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
// In your service configuration:
services
.AddConductorSharp(baseUrl: "http://localhost:8080")
.AddExecutionManager(...)
.AddSignalWait<YourSignalStore>("OPTIONAL_PREFIX"); // Implement ISignalStore for production
// Register the SignalWait workflow
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; } // Signal wait subworkflow
public CompleteOrderHandler CompleteOrder { get; set; }
public override void BuildDefinition()
{
// Process the order
_builder.AddTask(wf => wf.ProcessOrder, wf => new ProcessOrderRequest { OrderId = wf.WorkflowInput.OrderId });
// Wait for external payment confirmation signal
_builder.AddTask(wf => wf.WaitForPayment, wf => new SignalWaitInput
{
SignalKey = $"payment_{wf.WorkflowInput.OrderId}" // Unique key for this signal
});
// Continue after signal received
_builder.AddTask(wf => wf.CompleteOrder, wf => new CompleteOrderRequest
{
OrderId = wf.WorkflowInput.OrderId,
PaymentStatus = wf.WaitForPayment.Output.SignalStatus // Signal payload
});
}
}
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, // or TerminationStatus.Failed
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; }
// Add the human task
_builder.AddTask(wf => wf.HumanTask, wf => new HumanTaskInput<HumanTaskOutput> { });
// Use the human task output in subsequent tasks
_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:
// Requires: .AddConductorSharpPatterns()
public ReadWorkflowTasks ReadTasks { get; set; }
_builder.AddTask(
wf => wf.ReadTasks,
wf => new ReadWorkflowTasksInput
{
WorkflowId = wf.WorkflowInput.TargetWorkflowId,
TaskNames = "task1,task2" // Comma-separated task reference names
}
);
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, // Max concurrent task executions
sleepInterval: 500, // Base polling interval (ms)
longPollInterval: 100, // Long poll timeout (ms)
domain: "my-domain", // Optional worker domain
typeof(Program).Assembly // Assemblies containing handlers
);
Multiple Conductor Instances
services
.AddConductorSharp(baseUrl: "http://primary-conductor:8080")
.AddAlternateClient(
baseUrl: "http://secondary-conductor:8080",
key: "Secondary",
apiPath: "api",
ignoreInvalidCertificate: false
);
// Usage with keyed services
public class MyController(
IWorkflowService primaryService,
[FromKeyedServices("Secondary")] IWorkflowService secondaryService
) { }
Poll Timing Strategies
// Default: Inverse exponential backoff
.AddExecutionManager(...)
// Constant interval polling
.AddExecutionManager(...)
.UseConstantPollTimingStrategy()
Beta Execution Manager
.AddExecutionManager(...)
.UseBetaExecutionManager() // Uses TypePollSpreadingExecutionManager
Patterns Package
.AddExecutionManager(...)
.AddConductorSharpPatterns() // Adds WaitSeconds, ReadWorkflowTasks
.AddCSharpLambdaTasks() // Adds C# lambda task support
.AddSignalWait<YourSignalStore>() // Adds Signal Wait pattern (implement ISignalStore for production)
Kafka Cancellation Notifier
.AddExecutionManager(...)
.AddKafkaCancellationNotifier(
kafkaBootstrapServers: "localhost:9092",
topicName: "conductor.status.task",
groupId: "my-worker-group",
createTopicOnStartup: true
)
Pipeline Behaviors
.AddPipelines(pipelines =>
{
// Custom behavior (runs first)
pipelines.AddCustomBehavior(typeof(MyCustomBehavior<,>));
// Built-in behaviors
pipelines.AddExecutionTaskTracking(); // Track task execution metrics
pipelines.AddContextLogging(); // Add context to log scopes
pipelines.AddRequestResponseLogging(); // Log requests/responses
pipelines.AddValidation(); // Validate using DataAnnotations
})
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
// In Program.cs
builder.Services.AddHealthChecks()
.AddCheck<ConductorSharpHealthCheck>("conductor-worker");
// Configure health service
.AddExecutionManager(...)
.SetHealthCheckService<FileHealthService>() // or InMemoryHealthService
Client Services
Available services for direct Conductor API access:
IWorkflowService- Start, pause, resume, terminate workflowsITaskService- Update tasks, get logs, poll for tasksIMetadataService- Manage workflow/task definitionsIAdminService- Admin operations, queue managementIEventService- Event handlersIQueueAdminService- Queue administrationIWorkflowBulkService- Bulk workflow operationsIHealthService- Conductor server healthIExternalPayloadService- 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.0
Configuration
Create conductorsharp.yaml:
baseUrl: http://localhost:8080
apiPath: api
namespace: MyApp.Generated
destination: ./Generated
Usage
# Scaffold all tasks and workflows
dotnet-conductorsharp
# Use custom config file
dotnet-conductorsharp -f myconfig.yaml
# Filter by name
dotnet-conductorsharp -n CUSTOMER_get -n ORDER_create
# Filter by owner email
dotnet-conductorsharp -e team@example.com
# Filter by owner app
dotnet-conductorsharp -a my-application
# Skip tasks or workflows
dotnet-conductorsharp --no-tasks
dotnet-conductorsharp --no-workflows
# Preview without generating files
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 => { /* skip processing - no tasks */ },
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)