diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 47a169ed..048ecff4 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -21,3 +21,5 @@ jobs:
       run: dotnet restore ConductorSharp.sln
     - name: Build
       run: dotnet build --no-restore ConductorSharp.sln
+    - name: Test
+      run: dotnet test --no-restore --verbosity normal ConductorSharp.sln
\ No newline at end of file
diff --git a/ConductorSharp.sln b/ConductorSharp.sln
index 7f457c40..f0f8822f 100644
--- a/ConductorSharp.sln
+++ b/ConductorSharp.sln
@@ -1,67 +1,73 @@
-﻿
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.0.31919.166
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConductorSharp.Engine", "src\ConductorSharp.Engine\ConductorSharp.Engine.csproj", "{22BDC06E-50E5-4FE4-97D2-CD9E07EF4842}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConductorSharp.Client", "src\ConductorSharp.Client\ConductorSharp.Client.csproj", "{3244BEE8-17C4-4B00-BEB1-A41CC3E95482}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConductorSharp.ApiEnabled", "examples\ConductorSharp.ApiEnabled\ConductorSharp.ApiEnabled.csproj", "{C350E5E2-EA36-4816-BCBB-CBB597AAFF78}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConductorSharp.Definitions", "examples\ConductorSharp.Definitions\ConductorSharp.Definitions.csproj", "{68400F84-CE5A-4FFA-A417-05C798ECD5A4}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{16F68738-1ED3-4646-BE01-DB9246C10E89}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConductorSharp.NoApi", "examples\ConductorSharp.NoApi\ConductorSharp.NoApi.csproj", "{44E887F8-5E0D-4729-A4A8-AFEBBF87C6F9}"
-EndProject
-Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{C398B7D9-C238-4169-86A8-B03D01FBB2BE}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DE1FA421-B8D6-45A7-9667-0798A241A89C}"
-	ProjectSection(SolutionItems) = preProject
-		.editorconfig = .editorconfig
-	EndProjectSection
-EndProject
-Global
-	GlobalSection(SolutionConfigurationPlatforms) = preSolution
-		Debug|Any CPU = Debug|Any CPU
-		Release|Any CPU = Release|Any CPU
-	EndGlobalSection
-	GlobalSection(ProjectConfigurationPlatforms) = postSolution
-		{22BDC06E-50E5-4FE4-97D2-CD9E07EF4842}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{22BDC06E-50E5-4FE4-97D2-CD9E07EF4842}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{22BDC06E-50E5-4FE4-97D2-CD9E07EF4842}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{22BDC06E-50E5-4FE4-97D2-CD9E07EF4842}.Release|Any CPU.Build.0 = Release|Any CPU
-		{3244BEE8-17C4-4B00-BEB1-A41CC3E95482}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{3244BEE8-17C4-4B00-BEB1-A41CC3E95482}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{3244BEE8-17C4-4B00-BEB1-A41CC3E95482}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{3244BEE8-17C4-4B00-BEB1-A41CC3E95482}.Release|Any CPU.Build.0 = Release|Any CPU
-		{C350E5E2-EA36-4816-BCBB-CBB597AAFF78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{C350E5E2-EA36-4816-BCBB-CBB597AAFF78}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{C350E5E2-EA36-4816-BCBB-CBB597AAFF78}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{C350E5E2-EA36-4816-BCBB-CBB597AAFF78}.Release|Any CPU.Build.0 = Release|Any CPU
-		{68400F84-CE5A-4FFA-A417-05C798ECD5A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{68400F84-CE5A-4FFA-A417-05C798ECD5A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{68400F84-CE5A-4FFA-A417-05C798ECD5A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{68400F84-CE5A-4FFA-A417-05C798ECD5A4}.Release|Any CPU.Build.0 = Release|Any CPU
-		{44E887F8-5E0D-4729-A4A8-AFEBBF87C6F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{44E887F8-5E0D-4729-A4A8-AFEBBF87C6F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{44E887F8-5E0D-4729-A4A8-AFEBBF87C6F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{44E887F8-5E0D-4729-A4A8-AFEBBF87C6F9}.Release|Any CPU.Build.0 = Release|Any CPU
-		{C398B7D9-C238-4169-86A8-B03D01FBB2BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{C398B7D9-C238-4169-86A8-B03D01FBB2BE}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{C398B7D9-C238-4169-86A8-B03D01FBB2BE}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{C398B7D9-C238-4169-86A8-B03D01FBB2BE}.Release|Any CPU.Build.0 = Release|Any CPU
-	EndGlobalSection
-	GlobalSection(SolutionProperties) = preSolution
-		HideSolutionNode = FALSE
-	EndGlobalSection
-	GlobalSection(NestedProjects) = preSolution
-		{C350E5E2-EA36-4816-BCBB-CBB597AAFF78} = {16F68738-1ED3-4646-BE01-DB9246C10E89}
-		{68400F84-CE5A-4FFA-A417-05C798ECD5A4} = {16F68738-1ED3-4646-BE01-DB9246C10E89}
-		{44E887F8-5E0D-4729-A4A8-AFEBBF87C6F9} = {16F68738-1ED3-4646-BE01-DB9246C10E89}
-	EndGlobalSection
-	GlobalSection(ExtensibilityGlobals) = postSolution
-		SolutionGuid = {2443B3EC-E590-4C83-8BE2-A0787C66307C}
-	EndGlobalSection
-EndGlobal
+﻿
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31919.166
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConductorSharp.Engine", "src\ConductorSharp.Engine\ConductorSharp.Engine.csproj", "{22BDC06E-50E5-4FE4-97D2-CD9E07EF4842}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConductorSharp.Client", "src\ConductorSharp.Client\ConductorSharp.Client.csproj", "{3244BEE8-17C4-4B00-BEB1-A41CC3E95482}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConductorSharp.ApiEnabled", "examples\ConductorSharp.ApiEnabled\ConductorSharp.ApiEnabled.csproj", "{C350E5E2-EA36-4816-BCBB-CBB597AAFF78}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConductorSharp.Definitions", "examples\ConductorSharp.Definitions\ConductorSharp.Definitions.csproj", "{68400F84-CE5A-4FFA-A417-05C798ECD5A4}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{16F68738-1ED3-4646-BE01-DB9246C10E89}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConductorSharp.NoApi", "examples\ConductorSharp.NoApi\ConductorSharp.NoApi.csproj", "{44E887F8-5E0D-4729-A4A8-AFEBBF87C6F9}"
+EndProject
+Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{C398B7D9-C238-4169-86A8-B03D01FBB2BE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConductorSharp.Engine.Tests", "test\ConductorSharp.Engine.Tests\ConductorSharp.Engine.Tests.csproj", "{45AB7886-41DB-4A0E-9667-1C78D63F0FC7}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DE1FA421-B8D6-45A7-9667-0798A241A89C}"
+	ProjectSection(SolutionItems) = preProject
+		.editorconfig = .editorconfig
+	EndProjectSection
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{22BDC06E-50E5-4FE4-97D2-CD9E07EF4842}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{22BDC06E-50E5-4FE4-97D2-CD9E07EF4842}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{22BDC06E-50E5-4FE4-97D2-CD9E07EF4842}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{22BDC06E-50E5-4FE4-97D2-CD9E07EF4842}.Release|Any CPU.Build.0 = Release|Any CPU
+		{3244BEE8-17C4-4B00-BEB1-A41CC3E95482}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{3244BEE8-17C4-4B00-BEB1-A41CC3E95482}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{3244BEE8-17C4-4B00-BEB1-A41CC3E95482}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{3244BEE8-17C4-4B00-BEB1-A41CC3E95482}.Release|Any CPU.Build.0 = Release|Any CPU
+		{C350E5E2-EA36-4816-BCBB-CBB597AAFF78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{C350E5E2-EA36-4816-BCBB-CBB597AAFF78}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{C350E5E2-EA36-4816-BCBB-CBB597AAFF78}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{C350E5E2-EA36-4816-BCBB-CBB597AAFF78}.Release|Any CPU.Build.0 = Release|Any CPU
+		{68400F84-CE5A-4FFA-A417-05C798ECD5A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{68400F84-CE5A-4FFA-A417-05C798ECD5A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{68400F84-CE5A-4FFA-A417-05C798ECD5A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{68400F84-CE5A-4FFA-A417-05C798ECD5A4}.Release|Any CPU.Build.0 = Release|Any CPU
+		{44E887F8-5E0D-4729-A4A8-AFEBBF87C6F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{44E887F8-5E0D-4729-A4A8-AFEBBF87C6F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{44E887F8-5E0D-4729-A4A8-AFEBBF87C6F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{44E887F8-5E0D-4729-A4A8-AFEBBF87C6F9}.Release|Any CPU.Build.0 = Release|Any CPU
+		{C398B7D9-C238-4169-86A8-B03D01FBB2BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{C398B7D9-C238-4169-86A8-B03D01FBB2BE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{C398B7D9-C238-4169-86A8-B03D01FBB2BE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{C398B7D9-C238-4169-86A8-B03D01FBB2BE}.Release|Any CPU.Build.0 = Release|Any CPU
+		{45AB7886-41DB-4A0E-9667-1C78D63F0FC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{45AB7886-41DB-4A0E-9667-1C78D63F0FC7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{45AB7886-41DB-4A0E-9667-1C78D63F0FC7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{45AB7886-41DB-4A0E-9667-1C78D63F0FC7}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(NestedProjects) = preSolution
+		{C350E5E2-EA36-4816-BCBB-CBB597AAFF78} = {16F68738-1ED3-4646-BE01-DB9246C10E89}
+		{68400F84-CE5A-4FFA-A417-05C798ECD5A4} = {16F68738-1ED3-4646-BE01-DB9246C10E89}
+		{44E887F8-5E0D-4729-A4A8-AFEBBF87C6F9} = {16F68738-1ED3-4646-BE01-DB9246C10E89}
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {2443B3EC-E590-4C83-8BE2-A0787C66307C}
+	EndGlobalSection
+EndGlobal
diff --git a/test/ConductorSharp.Engine.Tests/ConductorSharp.Engine.Tests.csproj b/test/ConductorSharp.Engine.Tests/ConductorSharp.Engine.Tests.csproj
new file mode 100644
index 00000000..e3aa706a
--- /dev/null
+++ b/test/ConductorSharp.Engine.Tests/ConductorSharp.Engine.Tests.csproj
@@ -0,0 +1,38 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net6.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <IsPackable>false</IsPackable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <None Remove="Samples\Tasks\CustomerGet.json" />
+    <None Remove="Samples\Tasks\EmailPrepare.json" />
+    <None Remove="Samples\Workflows\SendCustomerNotification.json" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <EmbeddedResource Include="Samples\Tasks\CustomerGet.json" />
+    <EmbeddedResource Include="Samples\Tasks\EmailPrepare.json" />
+	<EmbeddedResource Include="Samples\Workflows\SendCustomerNotification.json" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
+    <PackageReference Include="xunit" Version="2.4.1" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
+    <PackageReference Include="coverlet.collector" Version="3.1.2">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\ConductorSharp.Engine\ConductorSharp.Engine.csproj" />
+  </ItemGroup>
+
+</Project>
diff --git a/test/ConductorSharp.Engine.Tests/Integration/WorkflowBuilderTests.cs b/test/ConductorSharp.Engine.Tests/Integration/WorkflowBuilderTests.cs
new file mode 100644
index 00000000..b2a725da
--- /dev/null
+++ b/test/ConductorSharp.Engine.Tests/Integration/WorkflowBuilderTests.cs
@@ -0,0 +1,16 @@
+﻿using ConductorSharp.Engine.Tests.Samples.Workflows;
+
+namespace ConductorSharp.Engine.Tests.Integration
+{
+    public class WorkflowBuilderTests
+    {
+        [Fact]
+        public void BuilderReturnsCorrectDefinition()
+        {
+            var definition = JsonConvert.SerializeObject(new SendCustomerNotification().GetDefinition(), Formatting.Indented);
+            var expectedDefinition = EmbeddedFileHelper.GetLinesFromEmbeddedFile("~/Samples/Workflows/SendCustomerNotification.json");
+
+            Assert.Equal(expectedDefinition, definition);
+        }
+    }
+}
diff --git a/test/ConductorSharp.Engine.Tests/Samples/Tasks/CustomerGet.json b/test/ConductorSharp.Engine.Tests/Samples/Tasks/CustomerGet.json
new file mode 100644
index 00000000..c801cab1
--- /dev/null
+++ b/test/ConductorSharp.Engine.Tests/Samples/Tasks/CustomerGet.json
@@ -0,0 +1,30 @@
+{
+  "ownerApp": "undefined",
+  "createTime": 0,
+  "updateTime": 0,
+  "createdBy": "UNDEFINED",
+  "updatedBy": "UNDEFINED",
+  "name": "CUSTOMER_get",
+  "description": "{\"description\":\"Missing description\"}",
+  "retryCount": 0,
+  "timeoutSeconds": 60,
+  "inputKeys": [
+    "customer_id"
+  ],
+  "outputKeys": [
+    "name",
+    "address"
+  ],
+  "timeoutPolicy": "TIME_OUT_WF",
+  "retryLogic": "FIXED",
+  "retryDelaySeconds": 0,
+  "responseTimeoutSeconds": 60,
+  "concurrentExecLimit": 0,
+  "inputTemplate": null,
+  "rateLimitPerFrequency": 0,
+  "rateLimitFrequencyInSeconds": 1,
+  "isolationGroupId": null,
+  "executionNameSpace": null,
+  "ownerEmail": "undefined@undefined.local",
+  "pollTimeoutSeconds": 0
+}
\ No newline at end of file
diff --git a/test/ConductorSharp.Engine.Tests/Samples/Tasks/CustomerGetV1.cs b/test/ConductorSharp.Engine.Tests/Samples/Tasks/CustomerGetV1.cs
new file mode 100644
index 00000000..c5c7d639
--- /dev/null
+++ b/test/ConductorSharp.Engine.Tests/Samples/Tasks/CustomerGetV1.cs
@@ -0,0 +1,31 @@
+﻿namespace ConductorSharp.Engine.Tests.Samples.Tasks;
+
+public partial class CustomerGetV1Input : IRequest<CustomerGetV1Output>
+{
+    /// <originalName>
+    /// customer_id
+    /// </originalName>
+    public dynamic CustomerId { get; set; }
+}
+
+public partial class CustomerGetV1Output
+{
+/// <originalName>
+/// name
+/// </originalName>
+public dynamic Name { get; set; }
+
+/// <originalName>
+/// address
+/// </originalName>
+public dynamic Address { get; set; }
+}
+
+/// <originalName>
+/// CUSTOMER_get
+/// </originalName>
+///
+[OriginalName("CUSTOMER_get")]
+public partial class CustomerGetV1 : SimpleTaskModel<CustomerGetV1Input, CustomerGetV1Output>
+{
+}
diff --git a/test/ConductorSharp.Engine.Tests/Samples/Tasks/EmailPrepare.json b/test/ConductorSharp.Engine.Tests/Samples/Tasks/EmailPrepare.json
new file mode 100644
index 00000000..50e935e3
--- /dev/null
+++ b/test/ConductorSharp.Engine.Tests/Samples/Tasks/EmailPrepare.json
@@ -0,0 +1,27 @@
+{
+  "ownerApp": "undefined",
+  "createTime": 1656663495488,
+  "createdBy": "",
+  "name": "EMAIL_prepare",
+  "description": "{\"description\":\"Missing description\"}",
+  "retryCount": 0,
+  "timeoutSeconds": 60,
+  "inputKeys": [
+    "customer_name",
+    "address"
+  ],
+  "outputKeys": [
+    "email_body"
+  ],
+  "timeoutPolicy": "TIME_OUT_WF",
+  "retryLogic": "FIXED",
+  "retryDelaySeconds": 0,
+  "responseTimeoutSeconds": 60,
+  "concurrentExecLimit": 0,
+  "inputTemplate": {},
+  "rateLimitPerFrequency": 0,
+  "rateLimitFrequencyInSeconds": 1,
+  "ownerEmail": "undefined@undefined.local",
+  "pollTimeoutSeconds": 0,
+  "backoffScaleFactor": 1
+}
\ No newline at end of file
diff --git a/test/ConductorSharp.Engine.Tests/Samples/Tasks/EmailPrepareV1.cs b/test/ConductorSharp.Engine.Tests/Samples/Tasks/EmailPrepareV1.cs
new file mode 100644
index 00000000..9ad9db2d
--- /dev/null
+++ b/test/ConductorSharp.Engine.Tests/Samples/Tasks/EmailPrepareV1.cs
@@ -0,0 +1,29 @@
+﻿namespace ConductorSharp.Engine.Tests.Samples.Tasks;
+
+public partial class EmailPrepareV1Input : IRequest<EmailPrepareV1Output>
+{
+    /// <originalName>
+    /// address
+    /// </originalName>
+    public dynamic Address { get; set; }
+    /// <originalName>
+    /// name
+    /// </originalName>
+    public dynamic Name { get; set; }
+}
+
+public partial class EmailPrepareV1Output
+{
+    /// <originalName>
+    /// email_body
+    /// </originalName>
+    public dynamic EmailBody { get; set; }
+}
+
+/// <originalName>
+/// EMAIL_prepare
+/// </originalName>
+[OriginalName("EMAIL_prepare")]
+public partial class EmailPrepareV1 : SimpleTaskModel<EmailPrepareV1Input, EmailPrepareV1Output>
+{
+}
diff --git a/test/ConductorSharp.Engine.Tests/Samples/Workers/GetCustomerHandler.cs b/test/ConductorSharp.Engine.Tests/Samples/Workers/GetCustomerHandler.cs
new file mode 100644
index 00000000..cf4c50a9
--- /dev/null
+++ b/test/ConductorSharp.Engine.Tests/Samples/Workers/GetCustomerHandler.cs
@@ -0,0 +1,28 @@
+﻿using System.ComponentModel.DataAnnotations;
+
+namespace ConductorSharp.Engine.Tests.Samples.Workers;
+
+public class GetCustomerRequest : IRequest<GetCustomerResponse>
+{
+    [Required]
+    public int CustomerId { get; set; }
+}
+
+public class GetCustomerResponse
+{
+    public string Name { get; set; }
+    public string Address { get; set; }
+}
+
+public class Customer
+{
+    public int Id { get; set; }
+    public string Name { get; set; }
+    public string Address { get; set; }
+}
+
+[OriginalName("CUSTOMER_get")]
+public class GetCustomerHandler : ITaskRequestHandler<GetCustomerRequest, GetCustomerResponse>
+{
+    public Task<GetCustomerResponse> Handle(GetCustomerRequest request, CancellationToken cancellationToken) => throw new NotImplementedException();
+}
diff --git a/test/ConductorSharp.Engine.Tests/Samples/Workers/PrepareEmailHandler.cs b/test/ConductorSharp.Engine.Tests/Samples/Workers/PrepareEmailHandler.cs
new file mode 100644
index 00000000..0c100b98
--- /dev/null
+++ b/test/ConductorSharp.Engine.Tests/Samples/Workers/PrepareEmailHandler.cs
@@ -0,0 +1,18 @@
+﻿namespace ConductorSharp.Engine.Tests.Samples.Workers;
+
+public class PrepareEmailRequest : IRequest<PrepareEmailResponse>
+{
+    public string CustomerName { get; set; }
+    public string Address { get; set; }
+}
+
+public class PrepareEmailResponse
+{
+    public string EmailBody { get; set; }
+}
+
+[OriginalName("EMAIL_prepare")]
+public class PrepareEmailHandler : ITaskRequestHandler<PrepareEmailRequest, PrepareEmailResponse>
+{
+    public Task<PrepareEmailResponse> Handle(PrepareEmailRequest request, CancellationToken cancellationToken) => throw new NotImplementedException();
+}
diff --git a/test/ConductorSharp.Engine.Tests/Samples/Workflows/SendCustomerNotification.cs b/test/ConductorSharp.Engine.Tests/Samples/Workflows/SendCustomerNotification.cs
new file mode 100644
index 00000000..2df608d2
--- /dev/null
+++ b/test/ConductorSharp.Engine.Tests/Samples/Workflows/SendCustomerNotification.cs
@@ -0,0 +1,47 @@
+﻿namespace ConductorSharp.Engine.Tests.Samples.Workflows;
+
+public class SendCustomerNotificationInput : WorkflowInput<SendCustomerNotificationOutput>
+{
+    public dynamic CustomerId { get; set; }
+}
+
+public class SendCustomerNotificationOutput : WorkflowOutput
+{
+    public dynamic EmailBody { get; set; }
+}
+
+[OriginalName("NOTIFICATION_send_to_customer")]
+public class SendCustomerNotification
+    : Workflow<SendCustomerNotificationInput, SendCustomerNotificationOutput>
+{
+    public CustomerGetV1 GetCustomer { get; set; }
+    public EmailPrepareV1 PrepareEmail { get; set; }
+
+    public override WorkflowDefinition GetDefinition()
+    {
+        var builder = new WorkflowDefinitionBuilder<SendCustomerNotification>();
+
+        builder.AddTask(
+            a => a.GetCustomer,
+            b => new() { CustomerId = b.WorkflowInput.CustomerId }
+        );
+
+        builder.AddTask(
+            a => a.PrepareEmail,
+            b =>
+                new()
+                {
+                    Address = b.GetCustomer.Output.Address,
+                    Name = b.GetCustomer.Output.Name
+                }
+        );
+
+        return builder.Build(
+            options =>
+            {
+                options.Version = 1;
+                options.OwnerEmail = "example@example.local";
+            }
+        );
+    }
+}
diff --git a/test/ConductorSharp.Engine.Tests/Samples/Workflows/SendCustomerNotification.json b/test/ConductorSharp.Engine.Tests/Samples/Workflows/SendCustomerNotification.json
new file mode 100644
index 00000000..83c0d144
--- /dev/null
+++ b/test/ConductorSharp.Engine.Tests/Samples/Workflows/SendCustomerNotification.json
@@ -0,0 +1,91 @@
+{
+  "ownerApp": null,
+  "createTime": 0,
+  "updateTime": 0,
+  "createdBy": null,
+  "updatedBy": null,
+  "name": "NOTIFICATION_send_to_customer",
+  "description": "{\"description\":null,\"labels\":null}",
+  "version": 1,
+  "tasks": [
+    {
+      "queryExpression": null,
+      "name": "CUSTOMER_get",
+      "taskReferenceName": "get_customer",
+      "description": "{\"description\":null}",
+      "inputParameters": {
+        "customer_id": "${workflow.input.customer_id}"
+      },
+      "type": "SIMPLE",
+      "dynamicTaskNameParam": null,
+      "caseValueParam": null,
+      "caseExpression": null,
+      "expression": null,
+      "evaluatorType": null,
+      "scriptExpression": null,
+      "decisionCases": null,
+      "dynamicForkJoinTasksParam": null,
+      "dynamicForkTasksParam": null,
+      "dynamicForkTasksInputParamName": null,
+      "defaultCase": null,
+      "forkTasks": null,
+      "startDelay": 0,
+      "subWorkflowParam": null,
+      "joinOn": null,
+      "sink": null,
+      "optional": false,
+      "taskDefinition": null,
+      "rateLimited": false,
+      "defaultExclusiveJoinTask": null,
+      "asyncComplete": false,
+      "loopCondition": null,
+      "loopOver": null
+    },
+    {
+      "queryExpression": null,
+      "name": "EMAIL_prepare",
+      "taskReferenceName": "prepare_email",
+      "description": "{\"description\":null}",
+      "inputParameters": {
+        "address": "${get_customer.output.address}",
+        "name": "${get_customer.output.name}"
+      },
+      "type": "SIMPLE",
+      "dynamicTaskNameParam": null,
+      "caseValueParam": null,
+      "caseExpression": null,
+      "expression": null,
+      "evaluatorType": null,
+      "scriptExpression": null,
+      "decisionCases": null,
+      "dynamicForkJoinTasksParam": null,
+      "dynamicForkTasksParam": null,
+      "dynamicForkTasksInputParamName": null,
+      "defaultCase": null,
+      "forkTasks": null,
+      "startDelay": 0,
+      "subWorkflowParam": null,
+      "joinOn": null,
+      "sink": null,
+      "optional": false,
+      "taskDefinition": null,
+      "rateLimited": false,
+      "defaultExclusiveJoinTask": null,
+      "asyncComplete": false,
+      "loopCondition": null,
+      "loopOver": null
+    }
+  ],
+  "inputParameters": [
+    "{\"customer_id\":{\"value\":\"\",\"description\":\" (optional)\"}}"
+  ],
+  "outputParameters": null,
+  "failureWorkflow": null,
+  "schemaVersion": 2,
+  "restartable": true,
+  "workflowStatusListenerEnabled": true,
+  "ownerEmail": "example@example.local",
+  "timeoutPolicy": null,
+  "timeoutSeconds": 0,
+  "variables": null
+}
\ No newline at end of file
diff --git a/test/ConductorSharp.Engine.Tests/Unit/TaskDefinitionBuilderTests.cs b/test/ConductorSharp.Engine.Tests/Unit/TaskDefinitionBuilderTests.cs
new file mode 100644
index 00000000..2cf2a99b
--- /dev/null
+++ b/test/ConductorSharp.Engine.Tests/Unit/TaskDefinitionBuilderTests.cs
@@ -0,0 +1,16 @@
+﻿using ConductorSharp.Engine.Tests.Samples.Workers;
+
+namespace ConductorSharp.Engine.Tests.Unit
+{
+    public class TaskDefinitionBuilderTests
+    {
+        [Fact]
+        public void ReturnsCorrectDefinition()
+        {
+            var definition = JsonConvert.SerializeObject(TaskDefinitionBuilder.Build<GetCustomerHandler>(null), Formatting.Indented);
+            var expectedDefinition = EmbeddedFileHelper.GetLinesFromEmbeddedFile("~/Samples/Tasks/CustomerGet.json");
+
+            Assert.Equal(expectedDefinition, definition);
+        }
+    }
+}
diff --git a/test/ConductorSharp.Engine.Tests/Usings.cs b/test/ConductorSharp.Engine.Tests/Usings.cs
new file mode 100644
index 00000000..4435fa02
--- /dev/null
+++ b/test/ConductorSharp.Engine.Tests/Usings.cs
@@ -0,0 +1,9 @@
+global using Xunit;
+global using ConductorSharp.Engine.Interface;
+global using ConductorSharp.Engine.Util;
+global using MediatR;
+global using ConductorSharp.Client.Model.Common;
+global using ConductorSharp.Engine.Builders;
+global using ConductorSharp.Engine.Tests.Samples.Tasks;
+global using ConductorSharp.Engine.Model;
+global using Newtonsoft.Json;
\ No newline at end of file
diff --git a/test/ConductorSharp.Engine.Tests/Util/EmbeddedFileHelper.cs b/test/ConductorSharp.Engine.Tests/Util/EmbeddedFileHelper.cs
new file mode 100644
index 00000000..52bcbfae
--- /dev/null
+++ b/test/ConductorSharp.Engine.Tests/Util/EmbeddedFileHelper.cs
@@ -0,0 +1,75 @@
+﻿using Newtonsoft.Json;
+using System;
+using System.IO;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ConductorSharp.Engine.Util
+{
+
+    internal static class EmbeddedFileHelper
+    {
+        private static string ReadAssemblyFile(Assembly assembly, string name)
+        {
+            var stream = assembly.GetManifestResourceStream(name);
+
+            if (stream == null)
+                throw new InvalidOperationException($"Resource {name} does not exist.");
+
+            using var reader = new StreamReader(stream, Encoding.UTF8);
+
+            return reader.ReadToEnd();
+        }
+
+        public static T GetObjectFromEmbeddedFile<T>(
+            string fileName,
+            params (string Key, object Value)[] templateParams
+        )
+        {
+            fileName = fileName.Replace(
+                    "~/",
+                    typeof(EmbeddedFileHelper).Assembly.GetName().Name + "."
+                )
+                .Replace("/", ".");
+
+            var contents = ReadAssemblyFile(typeof(EmbeddedFileHelper).Assembly, fileName);
+
+            if (contents == null)
+                throw new Exception();
+
+            if (templateParams != null)
+                foreach (var (Key, Value) in templateParams)
+                    contents = contents.Replace("{{" + Key + "}}", $"{Value}");
+
+            return JsonConvert.DeserializeObject<T>(contents);
+        }
+
+        public static string GetLinesFromEmbeddedFile(string fileName)
+        {
+            fileName = fileName.Replace(
+                    "~/",
+                    typeof(EmbeddedFileHelper).Assembly.GetName().Name + "."
+                )
+                .Replace("/", ".");
+
+            var contents = ReadAssemblyFile(typeof(EmbeddedFileHelper).Assembly, fileName);
+
+            return contents;
+        }
+
+        public static Task<T> GetObjectFromEmbeddedFileAsync<T>(
+            string fileName,
+            params (string Key, object Value)[] templateParams
+        ) => Task.FromResult(GetObjectFromEmbeddedFile<T>(fileName, templateParams));
+
+        public static string Reserialize<T>(
+            string fileName,
+            params (string Key, object Value)[] templateParams
+        )
+        {
+            var file = GetObjectFromEmbeddedFile<T>(fileName, templateParams);
+            return JsonConvert.SerializeObject(file);
+        }
+    }
+}
\ No newline at end of file
