diff --git a/README.md b/README.md index 3508358..fc6a5a8 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,10 @@ A comprehensive .NET client library for [Conductor](https://github.com/conductor [![NuGet](https://img.shields.io/nuget/v/ConductorSharp.Client.svg)](https://www.nuget.org/packages/ConductorSharp.Client) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) - **Note: This documentation has been AI generated and human reviewed.** +> **AI Assistant Users**: See [SKILL.md](SKILL.md) for a condensed reference guide optimized for AI coding assistants. It provides quick-reference documentation for all task types, configuration options, and common patterns. This file follows the [Agent Skills](https://code.claude.com/docs/en/skills) open standard for extending AI assistant capabilities. + ## Table of Contents - [Installation](#installation) @@ -26,6 +27,7 @@ A comprehensive .NET client library for [Conductor](https://github.com/conductor - [Toolkit CLI](#toolkit-cli) - [API Services](#api-services) - [Running the Examples](#running-the-examples) +- [General Notes](#general-notes) ## Installation @@ -141,19 +143,19 @@ public class SendNotificationWorkflow : Workflow wf.GetCustomer, - wf => new() { CustomerId = wf.WorkflowInput.CustomerId } + wf => new GetCustomerRequest { CustomerId = wf.WorkflowInput.CustomerId } ); _builder.AddTask( wf => wf.PrepareEmail, - wf => new() + wf => new PrepareEmailRequest { CustomerName = wf.GetCustomer.Output.Name, Address = wf.GetCustomer.Output.Address } ); - _builder.SetOutput(wf => new() + _builder.SetOutput(wf => new SendNotificationOutput { EmailBody = wf.PrepareEmail.Output.EmailBody }); @@ -180,11 +182,11 @@ public class MyWorkflow : Workflow wf.FirstTask, wf => new() { Input = wf.WorkflowInput.SomeValue }); - _builder.AddTask(wf => wf.SecondTask, wf => new() { Input = wf.FirstTask.Output.Result }); + _builder.AddTask(wf => wf.FirstTask, wf => new SomeTaskRequest { Input = wf.WorkflowInput.SomeValue }); + _builder.AddTask(wf => wf.SecondTask, wf => new AnotherTaskRequest { Input = wf.FirstTask.Output.Result }); // Set workflow output - _builder.SetOutput(wf => new() { Result = wf.SecondTask.Output.Value }); + _builder.SetOutput(wf => new MyWorkflowOutput { Result = wf.SecondTask.Output.Value }); } } ``` @@ -449,12 +451,22 @@ Note: `StringAddition` has an attribute `[OriginalName("string_addition")]` appl | `[Version(n)]` | Class | Version number for sub-workflow references | | `[TaskDomain("domain")]` | Class | Assign task to specific domain | +Note: There is no task equivalent of the `WorkflowMetadata` attribute. The task metadata is configured when registering the task: + +```csharp +services.RegisterWorkerTask(options => +{ + options.OwnerEmail = "team@example.com"; + options.Description = "My task description"; +}); +``` + ## Task Types ### Simple Task ```csharp -_builder.AddTask(wf => wf.MySimpleTask, wf => new() { Input = wf.WorkflowInput.Value }); +_builder.AddTask(wf => wf.MySimpleTask, wf => new MySimpleTaskRequest { Input = wf.WorkflowInput.Value }); ``` ### Sub-Workflow Task @@ -462,7 +474,7 @@ _builder.AddTask(wf => wf.MySimpleTask, wf => new() { Input = wf.WorkflowInput.V ```csharp public SubWorkflowTaskModel ChildWorkflow { get; set; } -_builder.AddTask(wf => wf.ChildWorkflow, wf => new() { CustomerId = wf.WorkflowInput.CustomerId }); +_builder.AddTask(wf => wf.ChildWorkflow, wf => new ChildWorkflowInput { CustomerId = wf.WorkflowInput.CustomerId }); ``` ### Switch Task (Conditional Branching) @@ -477,8 +489,8 @@ _builder.AddTask( wf => new SwitchTaskInput { SwitchCaseValue = wf.WorkflowInput.Operation }, new DecisionCases { - ["caseA"] = builder => builder.AddTask(wf => wf.TaskInCaseA, wf => new() { }), - ["caseB"] = builder => builder.AddTask(wf => wf.TaskInCaseB, wf => new() { }), + ["caseA"] = builder => builder.AddTask(wf => wf.TaskInCaseA, wf => new TaskARequest { }), + ["caseB"] = builder => builder.AddTask(wf => wf.TaskInCaseB, wf => new TaskBRequest { }), DefaultCase = builder => { /* default case tasks */ } } ); @@ -491,9 +503,9 @@ public DynamicTaskModel DynamicHandler { get; set _builder.AddTask( wf => wf.DynamicHandler, - wf => new() + wf => new DynamicTaskInput { - TaskInput = new() { CustomerId = wf.WorkflowInput.CustomerId }, + TaskInput = new ExpectedInput { CustomerId = wf.WorkflowInput.CustomerId }, TaskToExecute = wf.WorkflowInput.TaskName // Task name resolved at runtime } ); @@ -514,18 +526,70 @@ _builder.AddTask( ); ``` +### Do-While Loop Task + +```csharp +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", // Loop condition + builder => + { + builder.AddTask(wf => wf.GetCustomer, wf => new CustomerGetRequest { CustomerId = "CUSTOMER-1" }); + } +); +``` + +Note: ConductorSharp does not provide a strongly typed output for the `DoWhile` task, as can be seen from the implementation: + +```csharp +public class DoWhileTaskModel : TaskModel +{ +} +``` + ### Lambda Task (JavaScript) ```csharp +public class LambdaInput : IRequest +{ + public string Value { get; set; } +} + +public class LambdaOutput +{ + public string Something { get; set; } +} + public LambdaTaskModel LambdaTask { get; set; } + _builder.AddTask( wf => wf.LambdaTask, - wf => new() { Value = wf.WorkflowInput.Input }, - script: "return { result: $.Value.toUpperCase() }" + 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: + +```csharp +public abstract class LambdaOutputModel +{ + public O Result { get; set; } +} + +public abstract class LambdaTaskModel where I : IRequest +{ + public I Input { get; set; } + + public LambdaOutputModel Output { get; set; } +} +``` + ### Wait Task ```csharp @@ -552,18 +616,6 @@ _builder.AddTask( ); ``` -### Event Task - -```csharp -public EventTaskModel EventTask { get; set; } - -_builder.AddTask( - wf => wf.EventTask, - wf => new() { EventData = wf.WorkflowInput.Data }, - sink: "kafka:my-topic" -); -``` - ### Human Task ```csharp @@ -582,7 +634,7 @@ public JsonJqTransformTaskModel TransformTask { get; set; } _builder.AddTask( wf => wf.TransformTask, - wf => new() { QueryExpression = ".data | map(.name)", Data = wf.WorkflowInput.Items } + wf => new JqInput { QueryExpression = ".data | map(.name)", Data = wf.WorkflowInput.Items } ); ``` @@ -605,7 +657,7 @@ _builder.AddTasks(new WorkflowTask Mark tasks as optional (workflow continues on failure): ```csharp -_builder.AddTask(wf => wf.OptionalTask, wf => new() { }).AsOptional(); +_builder.AddTask(wf => wf.OptionalTask, wf => new OptionalTaskRequest { }).AsOptional(); ``` ## Configuration @@ -762,7 +814,7 @@ Additional built-in tasks and utilities: ```csharp public WaitSeconds WaitTask { get; set; } -_builder.AddTask(wf => wf.WaitTask, wf => new() { Seconds = 30 }); +_builder.AddTask(wf => wf.WaitTask, wf => new WaitSecondsRequest { Seconds = 30 }); ``` ### ReadWorkflowTasks Task @@ -774,7 +826,7 @@ public ReadWorkflowTasks ReadTasks { get; set; } _builder.AddTask( wf => wf.ReadTasks, - wf => new() + wf => new ReadWorkflowTasksInput { WorkflowId = wf.WorkflowInput.TargetWorkflowId, TaskNames = "task1,task2" // Comma-separated reference names @@ -791,7 +843,7 @@ public CSharpLambdaTaskModel InlineLambda { get; set; _builder.AddTask( wf => wf.InlineLambda, - wf => new() { Value = wf.WorkflowInput.Input }, + wf => new LambdaInput { Value = wf.WorkflowInput.Input }, input => new LambdaOutput { Result = input.Value.ToUpperInvariant() } ); ``` @@ -967,6 +1019,12 @@ cd examples/ConductorSharp.Definitions dotnet run ``` +## General Notes + +### Events Not Supported + +The Conductor events are currently not supported by the library. + ## License MIT License - see [LICENSE](LICENSE) for details. diff --git a/SKILL.md b/SKILL.md index 00dc1d3..c2e951c 100644 --- a/SKILL.md +++ b/SKILL.md @@ -135,15 +135,15 @@ public class MyWorkflow : Workflow wf.FirstTask, wf => new() { Input = wf.WorkflowInput.CustomerId }); - _builder.AddTask(wf => wf.SecondTask, wf => new() { Input = wf.FirstTask.Output.Result }); + _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() { Result = wf.SecondTask.Output.Value }); + _builder.SetOutput(wf => new MyWorkflowOutput { Result = wf.SecondTask.Output.Value }); } } ``` @@ -171,34 +171,43 @@ public class MyWorkflow : Workflow<...> { } ### Simple Task ```csharp -public MyTaskV1 MyTask { get; set; } +public MyTaskHandler MyTask { get; set; } -_builder.AddTask(wf => wf.MyTask, wf => new() { Input = wf.WorkflowInput.Value }); +_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`: + ```csharp -public SubWorkflowTaskModel ChildWorkflow { get; set; } +// Define the sub-workflow model (usually scaffolded or defined separately) +[OriginalName("CHILD_workflow")] +public class ChildWorkflow : SubWorkflowTaskModel { } + +// In the parent workflow: +public ChildWorkflow ChildWorkflow { get; set; } -_builder.AddTask(wf => wf.ChildWorkflow, wf => new() { CustomerId = wf.WorkflowInput.CustomerId }); +_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: + ```csharp public SwitchTaskModel SwitchTask { get; set; } -public TaskA TaskInCaseA { get; set; } -public TaskB TaskInCaseB { 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 { - ["caseA"] = builder => builder.AddTask(wf => wf.TaskInCaseA, wf => new() { }), - ["caseB"] = builder => builder.AddTask(wf => wf.TaskInCaseB, wf => new() { }), - DefaultCase = builder => { /* default case tasks */ } + ["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 }) } ); ``` @@ -208,14 +217,16 @@ _builder.AddTask( ```csharp #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() { CaseValueParam = "test" }, - new() + wf => new DecisionTaskInput { CaseValueParam = "test" }, + new DecisionCases { - ["test"] = builder => builder.AddTask(wf => wf.GetCustomer, wf => new() { CustomerId = 1 }), - DefaultCase = builder => builder.AddTask(wf => wf.Terminate, wf => new() { TerminationStatus = TerminationStatus.Failed }) + ["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 @@ -223,17 +234,44 @@ _builder.AddTask( ### 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: + ```csharp -public DynamicTaskModel DynamicHandler { get; set; } +// Define the expected input/output for the dynamic task +public class ExpectedDynamicInput : IRequest +{ + 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 +{ + public string TaskName { get; set; } // e.g., "CUSTOMER_get_v1" or "CUSTOMER_get_v2" + public int CustomerId { get; set; } +} + +public DynamicTaskModel DynamicHandler { get; set; } _builder.AddTask( wf => wf.DynamicHandler, - wf => new() + wf => new DynamicTaskInput { - TaskInput = new() { CustomerId = wf.WorkflowInput.CustomerId }, - TaskToExecute = wf.WorkflowInput.TaskName // Resolved at runtime + 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 @@ -253,68 +291,141 @@ _builder.AddTask( ### 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 + ```csharp +public class MyWorkflowInput : WorkflowInput +{ + public int Loops { get; set; } // Number of iterations +} + public DoWhileTaskModel DoWhile { get; set; } -public CustomerGetV1 GetCustomer { get; set; } +public CustomerGetHandler GetCustomer { get; set; } _builder.AddTask( wf => wf.DoWhile, - wf => new() { Value = wf.WorkflowInput.Loops }, - "$.do_while.iteration < $.value", // Loop condition + wf => new DoWhileInput { Value = wf.WorkflowInput.Loops }, // Value used in condition + "$.do_while.iteration < $.value", // Loop while iteration < Loops builder => { - builder.AddTask(wf => wf.GetCustomer, wf => new() { CustomerId = "CUSTOMER-1" }); + // 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: + +```csharp +public class DoWhileTaskModel : TaskModel +{ +} +``` + ### Lambda Task (JavaScript) +The Lambda task executes inline JavaScript code. Define input/output models: + ```csharp +public class LambdaInput : IRequest +{ + public string Value { get; set; } +} + +public class LambdaOutput +{ + public string Something { get; set; } +} + public LambdaTaskModel LambdaTask { get; set; } + _builder.AddTask( wf => wf.LambdaTask, - wf => new() { Value = wf.WorkflowInput.Input }, - script: "return { result: $.Value.toUpperCase() }" + 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: + +```csharp +public abstract class LambdaOutputModel +{ + public O Result { get; set; } +} + +public abstract class LambdaTaskModel where I : IRequest +{ + public I Input { get; set; } + + public LambdaOutputModel 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 +{ + public string Value { get; set; } +} + +public class LambdaOutput +{ + public string Result { get; set; } +} + public CSharpLambdaTaskModel InlineLambda { get; set; } _builder.AddTask( wf => wf.InlineLambda, - wf => new() { Value = wf.WorkflowInput.Input }, - input => new LambdaOutput { Result = input.Value.ToUpperInvariant() } + 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: + ```csharp 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 { Duration = "1h" } // or Until = "2024-01-01T00:00:00Z" + wf => new WaitTaskInput { Until = "2024-12-31 11:59" } ); ``` ### WaitSeconds Task (Patterns Package) +A convenience task for waiting a specific number of seconds: + ```csharp // Requires: .AddConductorSharpPatterns() public WaitSeconds WaitTask { get; set; } -_builder.AddTask(wf => wf.WaitTask, wf => new() { Seconds = 30 }); +_builder.AddTask(wf => wf.WaitTask, wf => new WaitSecondsRequest { Seconds = 30 }); ``` ### Terminate Task +The Terminate task ends the workflow execution with a specific status and output: + ```csharp public TerminateTaskModel TerminateTask { get; set; } @@ -322,43 +433,55 @@ _builder.AddTask( wf => wf.TerminateTask, wf => new TerminateTaskInput { - TerminationStatus = "COMPLETED", - WorkflowOutput = new { Result = "Done" } + TerminationStatus = TerminationStatus.Completed, // or TerminationStatus.Failed + WorkflowOutput = new { Property = "Test", Result = "Done" } } ); ``` -### Event Task - -```csharp -public EventTaskModel EventTask { get; set; } - -_builder.AddTask( - wf => wf.EventTask, - wf => new() { EventData = wf.WorkflowInput.Data }, - sink: "kafka:my-topic" -); -``` ### Human Task +The Human task pauses the workflow until a human completes an action (e.g., approval): + ```csharp +public class HumanTaskOutput +{ + public string CustomerId { get; set; } + public bool Approved { get; set; } +} + public HumanTaskModel HumanTask { get; set; } +public CustomerGetHandler GetCustomer { get; set; } -_builder.AddTask( - wf => wf.HumanTask, - wf => new HumanTaskInput { /* ... */ } -); +// Add the human task +_builder.AddTask(wf => wf.HumanTask, wf => new HumanTaskInput { }); + +// 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: + ```csharp +public class JqInput : IRequest +{ + public string QueryExpression { get; set; } + public object Data { get; set; } +} + +public class JqOutput +{ + public object Result { get; set; } +} + public JsonJqTransformTaskModel TransformTask { get; set; } _builder.AddTask( wf => wf.TransformTask, - wf => new() + wf => new JqInput { QueryExpression = ".data | map(.name)", Data = wf.WorkflowInput.Items @@ -368,24 +491,28 @@ _builder.AddTask( ### ReadWorkflowTasks Task (Patterns Package) +Reads task data from another workflow execution: + ```csharp // Requires: .AddConductorSharpPatterns() public ReadWorkflowTasks ReadTasks { get; set; } _builder.AddTask( wf => wf.ReadTasks, - wf => new() + wf => new ReadWorkflowTasksInput { WorkflowId = wf.WorkflowInput.TargetWorkflowId, - TaskNames = "task1,task2" // Comma-separated reference names + TaskNames = "task1,task2" // Comma-separated task reference names } ); ``` ### Optional Tasks +Mark a task as optional so workflow continues even if the task fails: + ```csharp -_builder.AddTask(wf => wf.OptionalTask, wf => new() { }).AsOptional(); +_builder.AddTask(wf => wf.OptionalTask, wf => new OptionalTaskRequest { Value = "test" }).AsOptional(); ``` ### PassThrough Task (Raw Definition) @@ -649,29 +776,36 @@ public class MyTaskHandler : TaskRequestHandler<...> { } ### Workflow with Multiple Tasks ```csharp +public GetCustomerHandler GetCustomer { get; set; } +public PrepareEmailHandler PrepareEmail { get; set; } + public override void BuildDefinition() { - _builder.AddTask(wf => wf.GetCustomer, wf => new() { CustomerId = wf.WorkflowInput.CustomerId }); - _builder.AddTask(wf => wf.PrepareEmail, wf => new() + _builder.AddTask(wf => wf.GetCustomer, wf => new GetCustomerRequest { CustomerId = wf.WorkflowInput.CustomerId }); + _builder.AddTask(wf => wf.PrepareEmail, wf => new PrepareEmailRequest { - CustomerName = wf.GetCustomer.Output.Name, + Name = wf.GetCustomer.Output.Name, Address = wf.GetCustomer.Output.Address }); - _builder.SetOutput(wf => new() { EmailBody = wf.PrepareEmail.Output.EmailBody }); + _builder.SetOutput(wf => new MyWorkflowOutput { EmailBody = wf.PrepareEmail.Output.EmailBody }); } ``` ### Conditional Workflow ```csharp +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 { - ["process"] = builder => builder.AddTask(wf => wf.ProcessTask, wf => new() { }), - ["skip"] = builder => { /* skip processing */ }, - DefaultCase = builder => builder.AddTask(wf => wf.DefaultTask, wf => new() { }) + ["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 { }) } ); ```