diff --git a/.github/workflows/elsa-studio.yml b/.github/workflows/elsa-studio.yml
deleted file mode 100644
index ebe40b11b..000000000
--- a/.github/workflows/elsa-studio.yml
+++ /dev/null
@@ -1,61 +0,0 @@
-name: Elsa Studio Docker image
-on:
- workflow_dispatch:
- push:
- branches:
- - develop/3.6.0
-
-jobs:
- push_to_registry:
- name: Push Docker image to Docker Hub
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout
- uses: actions/checkout@v3
-
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v2
- with:
- platforms: all
-
- - name: Set up Docker Buildx
- id: buildx
- uses: docker/setup-buildx-action@v2
-
- - name: Docker meta
- id: meta
- uses: docker/metadata-action@v3
- with:
- # list of Docker images to use as base name for tags
- images: |
- elsaworkflows/elsa-studio-v3-6-preview
- flavor: |
- latest=true
- # generate Docker tags based on the following events/attributes
- tags: |
- type=sha
-
- - name: Login to DockerHub
- if: github.event_name != 'pull_request'
- uses: docker/login-action@v2
- with:
- username: ${{ secrets.DOCKER_USER }}
- password: ${{ secrets.DOCKER_PASS }}
-
- - name: Build and push
- id: docker_build
- uses: docker/build-push-action@v3
- with:
- builder: ${{ steps.buildx.outputs.name }}
- context: .
- file: ./docker/ElsaStudio.Dockerfile
- platforms: linux/amd64,linux/arm64,linux/arm/v7
- push: ${{ github.event_name != 'pull_request' }}
- tags: ${{ steps.meta.outputs.tags }}
- labels: ${{ steps.meta.outputs.labels }}
- cache-from: type=local,src=/tmp/.buildx-cache
- cache-to: type=local,dest=/tmp/.buildx-cache
-
- - name: Image digest
- run: echo ${{ steps.docker_build.outputs.digest }}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 01970e991..baf28d767 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -133,6 +133,7 @@
+
diff --git a/Elsa.sln b/Elsa.sln
index 2d7a9e618..f8c579aff 100644
--- a/Elsa.sln
+++ b/Elsa.sln
@@ -302,6 +302,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Scheduling.UnitTests",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Common.IntegrationTests", "test\integration\Elsa.Common.IntegrationTests\Elsa.Common.IntegrationTests.csproj", "{6C451CC5-280E-475E-B95E-23ADCF0CCBFD}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Dsl.ElsaScript", "src\modules\Elsa.Dsl.ElsaScript\Elsa.Dsl.ElsaScript.csproj", "{874F5A44-DB06-47AB-A18C-2D13942E0147}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Dsl.ElsaScript.IntegrationTests", "test\integration\Elsa.Dsl.ElsaScript.IntegrationTests\Elsa.Dsl.ElsaScript.IntegrationTests.csproj", "{4B5A9D32-58F2-47A9-8A1B-8A9E2D76C3F5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.WorkflowProviders.BlobStorage.ElsaScript", "src\modules\Elsa.WorkflowProviders.BlobStorage.ElsaScript\Elsa.WorkflowProviders.BlobStorage.ElsaScript.csproj", "{2B7FB49D-E4B6-4AD5-981B-3D85B94F6F48}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "github", "github", "{06DC6553-AAEF-4FC8-B16F-039F292E86A3}"
ProjectSection(SolutionItems) = preProject
.github\copilot-instructions.md = .github\copilot-instructions.md
@@ -313,7 +319,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
.github\workflows\stale.yml = .github\workflows\stale.yml
.github\workflows\pr.yml = .github\workflows\pr.yml
.github\workflows\packages.yml = .github\workflows\packages.yml
- .github\workflows\elsa-studio.yml = .github\workflows\elsa-studio.yml
.github\workflows\elsa-server.yml = .github\workflows\elsa-server.yml
.github\workflows\docker-ca.yml = .github\workflows\docker-ca.yml
.github\workflows\bounty.yml = .github\workflows\bounty.yml
@@ -330,6 +335,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "issue_templates", "issue_te
.github\ISSUE_TEMPLATE\documentation.md = .github\ISSUE_TEMPLATE\documentation.md
EndProjectSection
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dsl", "dsl", "{477C2416-312D-46AE-BCD6-8FA1FAB43624}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -454,74 +461,6 @@ Global
{D5720DBC-8C2B-42D5-9D9F-2FF6EAD4001C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D5720DBC-8C2B-42D5-9D9F-2FF6EAD4001C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D5720DBC-8C2B-42D5-9D9F-2FF6EAD4001C}.Release|Any CPU.Build.0 = Release|Any CPU
- {BF934627-F531-44FB-BEC2-ECA801FF31E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {BF934627-F531-44FB-BEC2-ECA801FF31E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {BF934627-F531-44FB-BEC2-ECA801FF31E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {BF934627-F531-44FB-BEC2-ECA801FF31E7}.Release|Any CPU.Build.0 = Release|Any CPU
- {FD3CD5A8-E9B3-467F-90EB-2B7D5B83F348}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FD3CD5A8-E9B3-467F-90EB-2B7D5B83F348}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FD3CD5A8-E9B3-467F-90EB-2B7D5B83F348}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FD3CD5A8-E9B3-467F-90EB-2B7D5B83F348}.Release|Any CPU.Build.0 = Release|Any CPU
- {3BED411B-79B5-4CCC-BD46-9549A427B908}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3BED411B-79B5-4CCC-BD46-9549A427B908}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3BED411B-79B5-4CCC-BD46-9549A427B908}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3BED411B-79B5-4CCC-BD46-9549A427B908}.Release|Any CPU.Build.0 = Release|Any CPU
- {6CC5FBC7-D3D7-4FE3-AD08-C67939BDB24D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {6CC5FBC7-D3D7-4FE3-AD08-C67939BDB24D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {6CC5FBC7-D3D7-4FE3-AD08-C67939BDB24D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {6CC5FBC7-D3D7-4FE3-AD08-C67939BDB24D}.Release|Any CPU.Build.0 = Release|Any CPU
- {FA5E857F-B173-4B5D-8049-B817A210DEF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FA5E857F-B173-4B5D-8049-B817A210DEF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FA5E857F-B173-4B5D-8049-B817A210DEF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FA5E857F-B173-4B5D-8049-B817A210DEF5}.Release|Any CPU.Build.0 = Release|Any CPU
- {A51F9683-DA9F-45E7-82DE-1E261ACD6D68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A51F9683-DA9F-45E7-82DE-1E261ACD6D68}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A51F9683-DA9F-45E7-82DE-1E261ACD6D68}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A51F9683-DA9F-45E7-82DE-1E261ACD6D68}.Release|Any CPU.Build.0 = Release|Any CPU
- {75E08B0E-A41A-425C-9DCA-155505CE41CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {75E08B0E-A41A-425C-9DCA-155505CE41CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {75E08B0E-A41A-425C-9DCA-155505CE41CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {75E08B0E-A41A-425C-9DCA-155505CE41CD}.Release|Any CPU.Build.0 = Release|Any CPU
- {BD120D97-4862-469E-B2CF-4109F7F21C20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {BD120D97-4862-469E-B2CF-4109F7F21C20}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {BD120D97-4862-469E-B2CF-4109F7F21C20}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {BD120D97-4862-469E-B2CF-4109F7F21C20}.Release|Any CPU.Build.0 = Release|Any CPU
- {8A3443F3-D40E-44B9-9805-B4CB7A3F022C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {8A3443F3-D40E-44B9-9805-B4CB7A3F022C}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {8A3443F3-D40E-44B9-9805-B4CB7A3F022C}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {8A3443F3-D40E-44B9-9805-B4CB7A3F022C}.Release|Any CPU.Build.0 = Release|Any CPU
- {FA2B2B44-7E3E-444B-A157-7526B376E9CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FA2B2B44-7E3E-444B-A157-7526B376E9CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FA2B2B44-7E3E-444B-A157-7526B376E9CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FA2B2B44-7E3E-444B-A157-7526B376E9CA}.Release|Any CPU.Build.0 = Release|Any CPU
- {DF0CA7E2-F5DF-40ED-91B6-748767084FC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {DF0CA7E2-F5DF-40ED-91B6-748767084FC7}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {DF0CA7E2-F5DF-40ED-91B6-748767084FC7}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {DF0CA7E2-F5DF-40ED-91B6-748767084FC7}.Release|Any CPU.Build.0 = Release|Any CPU
- {DB20B168-CD90-41F2-BE08-3D8C6F142459}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {DB20B168-CD90-41F2-BE08-3D8C6F142459}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {DB20B168-CD90-41F2-BE08-3D8C6F142459}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {DB20B168-CD90-41F2-BE08-3D8C6F142459}.Release|Any CPU.Build.0 = Release|Any CPU
- {70593549-8B26-4D63-9857-6BA8BB3E31DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {70593549-8B26-4D63-9857-6BA8BB3E31DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {70593549-8B26-4D63-9857-6BA8BB3E31DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {70593549-8B26-4D63-9857-6BA8BB3E31DB}.Release|Any CPU.Build.0 = Release|Any CPU
- {E7137FB0-1988-4562-AD8D-D0D9D2EE85F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E7137FB0-1988-4562-AD8D-D0D9D2EE85F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E7137FB0-1988-4562-AD8D-D0D9D2EE85F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E7137FB0-1988-4562-AD8D-D0D9D2EE85F6}.Release|Any CPU.Build.0 = Release|Any CPU
- {EB24F9FE-D7BD-4FCC-907E-AE400288C2A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {EB24F9FE-D7BD-4FCC-907E-AE400288C2A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {EB24F9FE-D7BD-4FCC-907E-AE400288C2A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {EB24F9FE-D7BD-4FCC-907E-AE400288C2A5}.Release|Any CPU.Build.0 = Release|Any CPU
- {9CA02818-F7EB-4A0B-B27B-BC74ACD499C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {9CA02818-F7EB-4A0B-B27B-BC74ACD499C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {9CA02818-F7EB-4A0B-B27B-BC74ACD499C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {9CA02818-F7EB-4A0B-B27B-BC74ACD499C9}.Release|Any CPU.Build.0 = Release|Any CPU
- {C583AF05-D517-4B7F-8955-6B61500ED3D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C583AF05-D517-4B7F-8955-6B61500ED3D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C583AF05-D517-4B7F-8955-6B61500ED3D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C583AF05-D517-4B7F-8955-6B61500ED3D8}.Release|Any CPU.Build.0 = Release|Any CPU
{6F14B066-DF7B-2409-59D8-CCCA90A4974C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6F14B066-DF7B-2409-59D8-CCCA90A4974C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6F14B066-DF7B-2409-59D8-CCCA90A4974C}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -642,6 +581,18 @@ Global
{6C451CC5-280E-475E-B95E-23ADCF0CCBFD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6C451CC5-280E-475E-B95E-23ADCF0CCBFD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6C451CC5-280E-475E-B95E-23ADCF0CCBFD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {874F5A44-DB06-47AB-A18C-2D13942E0147}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {874F5A44-DB06-47AB-A18C-2D13942E0147}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {874F5A44-DB06-47AB-A18C-2D13942E0147}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {874F5A44-DB06-47AB-A18C-2D13942E0147}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4B5A9D32-58F2-47A9-8A1B-8A9E2D76C3F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4B5A9D32-58F2-47A9-8A1B-8A9E2D76C3F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4B5A9D32-58F2-47A9-8A1B-8A9E2D76C3F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4B5A9D32-58F2-47A9-8A1B-8A9E2D76C3F5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2B7FB49D-E4B6-4AD5-981B-3D85B94F6F48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2B7FB49D-E4B6-4AD5-981B-3D85B94F6F48}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2B7FB49D-E4B6-4AD5-981B-3D85B94F6F48}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2B7FB49D-E4B6-4AD5-981B-3D85B94F6F48}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -740,6 +691,10 @@ Global
{6C451CC5-280E-475E-B95E-23ADCF0CCBFD} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
{2158FC82-6253-47F2-9216-B266710D50F1} = {06DC6553-AAEF-4FC8-B16F-039F292E86A3}
{36556068-64BF-4D4B-827E-EC8CDC23D516} = {06DC6553-AAEF-4FC8-B16F-039F292E86A3}
+ {4B5A9D32-58F2-47A9-8A1B-8A9E2D76C3F5} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
+ {2B7FB49D-E4B6-4AD5-981B-3D85B94F6F48} = {B08B4E00-C2AB-48F3-8389-449F42AEF179}
+ {477C2416-312D-46AE-BCD6-8FA1FAB43624} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
+ {874F5A44-DB06-47AB-A18C-2D13942E0147} = {477C2416-312D-46AE-BCD6-8FA1FAB43624}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
diff --git a/build/_build.csproj b/build/_build.csproj
index bd32549c9..dac12855f 100644
--- a/build/_build.csproj
+++ b/build/_build.csproj
@@ -1,4 +1,4 @@
-
+
Exe
diff --git a/run-dsl-tests.sh b/run-dsl-tests.sh
new file mode 100644
index 000000000..99e4b3b6d
--- /dev/null
+++ b/run-dsl-tests.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+# Run from the repository root, or adjust the path below as needed.
+dotnet test test/integration/Elsa.Dsl.ElsaScript.IntegrationTests/Elsa.Dsl.ElsaScript.IntegrationTests.csproj --verbosity normal
diff --git a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj
index 6fa3939e8..206866f3d 100644
--- a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj
+++ b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj
@@ -3,9 +3,12 @@
+
+
+
@@ -24,4 +27,19 @@
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+
diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs
index 2f6c5658a..b056f5010 100644
--- a/src/apps/Elsa.Server.Web/Program.cs
+++ b/src/apps/Elsa.Server.Web/Program.cs
@@ -8,6 +8,10 @@ using Elsa.Persistence.EFCore.Extensions;
using Elsa.Persistence.EFCore.Modules.Management;
using Elsa.Persistence.EFCore.Modules.Runtime;
using Elsa.Server.Web.Filters;
+using Elsa.Tenants.AspNetCore;
+using Elsa.Tenants.Extensions;
+using Elsa.WorkflowProviders.BlobStorage.ElsaScript.Extensions;
+using Elsa.Workflows;
using Elsa.Workflows.Api;
using Elsa.Workflows.CommitStates.Strategies;
using Elsa.Workflows.IncidentStrategies;
@@ -70,6 +74,8 @@ services
runtime.UseDistributedRuntime();
})
.UseWorkflowsApi()
+ .UseFluentStorageProvider()
+ .UseElsaScriptBlobStorage()
.UseScheduling()
.UseCSharp(options =>
{
diff --git a/src/apps/Elsa.Server.Web/Workflows/flowchart-test.elsa b/src/apps/Elsa.Server.Web/Workflows/flowchart-test.elsa
new file mode 100644
index 000000000..39439a8da
--- /dev/null
+++ b/src/apps/Elsa.Server.Web/Workflows/flowchart-test.elsa
@@ -0,0 +1,16 @@
+use Elsa.Activities.Console;
+
+workflow FlowchartTest {
+ flowchart {
+ var counter = 0;
+
+ entry Start;
+
+ Start: WriteLine("Starting flowchart");
+ CheckCondition: WriteLine("Checking condition");
+ EndNode: WriteLine("Done!");
+
+ Start -> CheckCondition;
+ CheckCondition.Done -> EndNode;
+ }
+}
diff --git a/src/apps/Elsa.Server.Web/Workflows/for-loop.elsa b/src/apps/Elsa.Server.Web/Workflows/for-loop.elsa
new file mode 100644
index 000000000..cad2bb7df
--- /dev/null
+++ b/src/apps/Elsa.Server.Web/Workflows/for-loop.elsa
@@ -0,0 +1,18 @@
+use expressions js;
+
+workflow ForLoopTest
+{
+ WriteLine("First loop")
+
+ for (var i = 0 through 10 step 1)
+ {
+ WriteLine(js => `Step: ${variables.i}`)
+ }
+
+ WriteLine("Second loop")
+
+ for (var i = 0 to 10 step 2)
+ WriteLine(cs => return $"Step: {Variables.Get("i")}";)
+
+ WriteLine("Done!")
+}
\ No newline at end of file
diff --git a/src/apps/Elsa.Server.Web/Workflows/hello-world-http.elsa b/src/apps/Elsa.Server.Web/Workflows/hello-world-http.elsa
new file mode 100644
index 000000000..f4bb0db7d
--- /dev/null
+++ b/src/apps/Elsa.Server.Web/Workflows/hello-world-http.elsa
@@ -0,0 +1,8 @@
+use expressions js;
+
+workflow HelloWorldHttpDsl
+{
+ var message = "Hello World from DSL via Expressions!";
+ listen HttpEndpoint("/hello-world-dsl");
+ WriteHttpResponse(js => `Message: ${variables.message}`);
+}
\ No newline at end of file
diff --git a/src/apps/Elsa.Server.Web/Workflows/hello-world.elsa b/src/apps/Elsa.Server.Web/Workflows/hello-world.elsa
new file mode 100644
index 000000000..37a88a681
--- /dev/null
+++ b/src/apps/Elsa.Server.Web/Workflows/hello-world.elsa
@@ -0,0 +1,6 @@
+use expressions js;
+
+workflow HelloWorldDsl
+{
+ WriteLine("Hello World from Elsa DSL!");
+}
\ No newline at end of file
diff --git a/src/apps/Elsa.Server.Web/Workflows/multi-workflow-example.elsa b/src/apps/Elsa.Server.Web/Workflows/multi-workflow-example.elsa
new file mode 100644
index 000000000..05998558e
--- /dev/null
+++ b/src/apps/Elsa.Server.Web/Workflows/multi-workflow-example.elsa
@@ -0,0 +1,29 @@
+use Elsa.Activities.Console;
+
+workflow HelloWorldDsl(
+ DisplayName: "Hello World DSL",
+ Description: "Demonstrates a simple ElsaScript workflow with metadata.",
+ DefinitionId: "hello-world-dsl",
+ DefinitionVersionId: "hello-world-dsl-v1",
+ Version: 1,
+ UsableAsActivity: true
+)
+{
+ use expressions js;
+
+ WriteLine("Hello World from Elsa DSL!");
+}
+
+workflow HelloWorldAsActivity(
+ DisplayName: "Hello World as Activity",
+ Description: "Can be embedded as an activity in other workflows.",
+ DefinitionId: "hello-world-activity",
+ DefinitionVersionId: "hello-world-activity-v1",
+ Version: 1,
+ UsableAsActivity: true
+)
+{
+ use expressions js;
+
+ WriteLine(`Inner hello at ${Date.now()}`);
+}
diff --git a/src/apps/Elsa.Server.Web/appsettings.json b/src/apps/Elsa.Server.Web/appsettings.json
index f7456f2df..3bdf4969d 100644
--- a/src/apps/Elsa.Server.Web/appsettings.json
+++ b/src/apps/Elsa.Server.Web/appsettings.json
@@ -3,7 +3,8 @@
"LogLevel": {
"Default": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
- "Elsa": "Debug"
+ "Elsa": "Warning",
+ "Elsa.Workflows.ActivityRegistry": "Error"
}
},
"HostBuilder": {
diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Requests/ListWorkflowDefinitionsRequest.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Requests/ListWorkflowDefinitionsRequest.cs
index 46bf0403b..6c44cc588 100644
--- a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Requests/ListWorkflowDefinitionsRequest.cs
+++ b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Requests/ListWorkflowDefinitionsRequest.cs
@@ -2,8 +2,7 @@ using Elsa.Api.Client.Resources.WorkflowDefinitions.Enums;
using Elsa.Api.Client.Shared.Models;
using Refit;
-namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Requests
-;
+namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Requests;
///
/// Represents a request to list workflow definitions.
@@ -63,4 +62,4 @@ public class ListWorkflowDefinitionsRequest
/// The order direction.
///
public OrderDirection? OrderDirection { get; set; }
-}
\ No newline at end of file
+}
diff --git a/src/common/Elsa.Testing.Shared/ActivityTestFixture.cs b/src/common/Elsa.Testing.Shared/ActivityTestFixture.cs
index 7a163e36e..aaecccf79 100644
--- a/src/common/Elsa.Testing.Shared/ActivityTestFixture.cs
+++ b/src/common/Elsa.Testing.Shared/ActivityTestFixture.cs
@@ -153,7 +153,6 @@ public class ActivityTestFixture
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddScoped();
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Ast/ActivityInvocationNode.cs b/src/modules/Elsa.Dsl.ElsaScript/Ast/ActivityInvocationNode.cs
new file mode 100644
index 000000000..81e80c393
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Ast/ActivityInvocationNode.cs
@@ -0,0 +1,22 @@
+namespace Elsa.Dsl.ElsaScript.Ast;
+
+///
+/// Represents an activity invocation.
+///
+public class ActivityInvocationNode : StatementNode
+{
+ ///
+ /// The activity name (e.g., "WriteLine", "SendEmail").
+ ///
+ public string ActivityName { get; set; } = string.Empty;
+
+ ///
+ /// The arguments passed to the activity.
+ ///
+ public List Arguments { get; set; } = [];
+
+ ///
+ /// Optional variable to capture the result.
+ ///
+ public string? ResultVariable { get; set; }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Ast/ArgumentNode.cs b/src/modules/Elsa.Dsl.ElsaScript/Ast/ArgumentNode.cs
new file mode 100644
index 000000000..327763e8a
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Ast/ArgumentNode.cs
@@ -0,0 +1,17 @@
+namespace Elsa.Dsl.ElsaScript.Ast;
+
+///
+/// Represents an argument passed to an activity.
+///
+public class ArgumentNode : AstNode
+{
+ ///
+ /// The parameter name (null for positional arguments).
+ ///
+ public string? Name { get; set; }
+
+ ///
+ /// The argument value expression.
+ ///
+ public ExpressionNode Value { get; set; } = null!;
+}
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Ast/AstNode.cs b/src/modules/Elsa.Dsl.ElsaScript/Ast/AstNode.cs
new file mode 100644
index 000000000..e9f210d61
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Ast/AstNode.cs
@@ -0,0 +1,17 @@
+namespace Elsa.Dsl.ElsaScript.Ast;
+
+///
+/// Base class for all AST nodes in ElsaScript.
+///
+public abstract class AstNode
+{
+ ///
+ /// The line number in the source where this node starts.
+ ///
+ public int Line { get; set; }
+
+ ///
+ /// The column number in the source where this node starts.
+ ///
+ public int Column { get; set; }
+}
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Ast/BlockNode.cs b/src/modules/Elsa.Dsl.ElsaScript/Ast/BlockNode.cs
new file mode 100644
index 000000000..5ca38de9d
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Ast/BlockNode.cs
@@ -0,0 +1,12 @@
+namespace Elsa.Dsl.ElsaScript.Ast;
+
+///
+/// Represents a block of statements (maps to Sequence).
+///
+public class BlockNode : StatementNode
+{
+ ///
+ /// The statements in the block.
+ ///
+ public List Statements { get; set; } = [];
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Ast/ConnectionNode.cs b/src/modules/Elsa.Dsl.ElsaScript/Ast/ConnectionNode.cs
new file mode 100644
index 000000000..88dcc50c8
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Ast/ConnectionNode.cs
@@ -0,0 +1,22 @@
+namespace Elsa.Dsl.ElsaScript.Ast;
+
+///
+/// Represents a connection in a flowchart.
+///
+public class ConnectionNode : AstNode
+{
+ ///
+ /// The source label.
+ ///
+ public string Source { get; set; } = string.Empty;
+
+ ///
+ /// The optional outcome.
+ ///
+ public string? Outcome { get; set; }
+
+ ///
+ /// The target label.
+ ///
+ public string Target { get; set; } = string.Empty;
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Ast/ExpressionNode.cs b/src/modules/Elsa.Dsl.ElsaScript/Ast/ExpressionNode.cs
new file mode 100644
index 000000000..819e98e0a
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Ast/ExpressionNode.cs
@@ -0,0 +1,127 @@
+namespace Elsa.Dsl.ElsaScript.Ast;
+
+///
+/// Base class for expression nodes.
+///
+public abstract class ExpressionNode : AstNode
+{
+}
+
+///
+/// Represents a literal expression.
+///
+public class LiteralNode : ExpressionNode
+{
+ ///
+ /// The literal value.
+ ///
+ public object? Value { get; set; }
+}
+
+///
+/// Represents an Elsa expression with a specific language.
+///
+public class ElsaExpressionNode : ExpressionNode
+{
+ ///
+ /// The expression language (e.g., "js", "cs", "py", "liquid").
+ /// If null, uses the default language from use statements.
+ ///
+ public string? Language { get; set; }
+
+ ///
+ /// The expression code.
+ ///
+ public string Expression { get; set; } = string.Empty;
+}
+
+///
+/// Represents a template string expression.
+///
+public class TemplateStringNode : ExpressionNode
+{
+ ///
+ /// The parts of the template string (alternating between literals and expressions).
+ ///
+ public List Parts { get; set; } = [];
+}
+
+///
+/// Represents a part of a template string.
+///
+public class TemplatePartNode : AstNode
+{
+ ///
+ /// Whether this is a literal or expression part.
+ ///
+ public bool IsExpression { get; set; }
+
+ ///
+ /// The content (literal text or expression code).
+ ///
+ public string Content { get; set; } = string.Empty;
+}
+
+///
+/// Represents an identifier (variable reference).
+///
+public class IdentifierNode : ExpressionNode
+{
+ ///
+ /// The identifier name.
+ ///
+ public string Name { get; set; } = string.Empty;
+}
+
+///
+/// Represents an array literal.
+///
+public class ArrayLiteralNode : ExpressionNode
+{
+ ///
+ /// The elements of the array.
+ ///
+ public List Elements { get; set; } = [];
+}
+
+///
+/// Represents a binary expression (e.g., a + b).
+///
+public class BinaryExpressionNode : ExpressionNode
+{
+ ///
+ /// The left operand.
+ ///
+ public ExpressionNode Left { get; set; } = null!;
+
+ ///
+ /// The operator.
+ ///
+ public string Operator { get; set; } = string.Empty;
+
+ ///
+ /// The right operand.
+ ///
+ public ExpressionNode Right { get; set; } = null!;
+}
+
+///
+/// Represents a unary expression (e.g., ++i, i++).
+///
+public class UnaryExpressionNode : ExpressionNode
+{
+ ///
+ /// The operator.
+ ///
+ public string Operator { get; set; } = string.Empty;
+
+ ///
+ /// The operand.
+ ///
+ public ExpressionNode Operand { get; set; } = null!;
+
+ ///
+ /// Whether the operator is prefix (true) or postfix (false).
+ ///
+ public bool IsPrefix { get; set; }
+}
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Ast/FlowchartNode.cs b/src/modules/Elsa.Dsl.ElsaScript/Ast/FlowchartNode.cs
new file mode 100644
index 000000000..cfbbc10e0
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Ast/FlowchartNode.cs
@@ -0,0 +1,27 @@
+namespace Elsa.Dsl.ElsaScript.Ast;
+
+///
+/// Represents a flowchart definition.
+///
+public class FlowchartNode : StatementNode
+{
+ ///
+ /// Variable declarations scoped to the flowchart.
+ ///
+ public List Variables { get; set; } = [];
+
+ ///
+ /// The labeled activities (nodes) in the flowchart.
+ ///
+ public List Activities { get; set; } = [];
+
+ ///
+ /// The connections (edges) between activities.
+ ///
+ public List Connections { get; set; } = [];
+
+ ///
+ /// The optional entry point label.
+ ///
+ public string? EntryPoint { get; set; }
+}
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Ast/ForEachNode.cs b/src/modules/Elsa.Dsl.ElsaScript/Ast/ForEachNode.cs
new file mode 100644
index 000000000..99fe0d6dc
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Ast/ForEachNode.cs
@@ -0,0 +1,27 @@
+namespace Elsa.Dsl.ElsaScript.Ast;
+
+///
+/// Represents a foreach statement (e.g., foreach (var item in collection)).
+///
+public class ForEachNode : StatementNode
+{
+ ///
+ /// Whether this foreach loop declares a new variable (var present in header).
+ ///
+ public bool DeclaresVariable { get; set; }
+
+ ///
+ /// The loop variable name.
+ ///
+ public string VariableName { get; set; } = null!;
+
+ ///
+ /// The collection expression to iterate over.
+ ///
+ public ExpressionNode Collection { get; set; } = null!;
+
+ ///
+ /// The body statement.
+ ///
+ public StatementNode Body { get; set; } = null!;
+}
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Ast/ForNode.cs b/src/modules/Elsa.Dsl.ElsaScript/Ast/ForNode.cs
new file mode 100644
index 000000000..953920208
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Ast/ForNode.cs
@@ -0,0 +1,42 @@
+namespace Elsa.Dsl.ElsaScript.Ast;
+
+///
+/// Represents a range-based for statement (e.g., for (var i = 0 to 10 step 1)).
+///
+public class ForNode : StatementNode
+{
+ ///
+ /// Whether this for loop declares a new variable (var present in header).
+ ///
+ public bool DeclaresVariable { get; set; }
+
+ ///
+ /// The loop variable name.
+ ///
+ public string VariableName { get; set; } = null!;
+
+ ///
+ /// The start value expression.
+ ///
+ public ExpressionNode Start { get; set; } = null!;
+
+ ///
+ /// The end value expression.
+ ///
+ public ExpressionNode End { get; set; } = null!;
+
+ ///
+ /// The step value expression.
+ ///
+ public ExpressionNode Step { get; set; } = null!;
+
+ ///
+ /// Whether the end value is inclusive (through) or exclusive (to).
+ ///
+ public bool IsInclusive { get; set; }
+
+ ///
+ /// The body statement.
+ ///
+ public StatementNode Body { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Ast/IfNode.cs b/src/modules/Elsa.Dsl.ElsaScript/Ast/IfNode.cs
new file mode 100644
index 000000000..d3fbaf8f7
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Ast/IfNode.cs
@@ -0,0 +1,22 @@
+namespace Elsa.Dsl.ElsaScript.Ast;
+
+///
+/// Represents an if statement.
+///
+public class IfNode : StatementNode
+{
+ ///
+ /// The condition expression.
+ ///
+ public ExpressionNode Condition { get; set; } = null!;
+
+ ///
+ /// The then branch.
+ ///
+ public StatementNode Then { get; set; } = null!;
+
+ ///
+ /// The optional else branch.
+ ///
+ public StatementNode? Else { get; set; }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Ast/LabeledActivityNode.cs b/src/modules/Elsa.Dsl.ElsaScript/Ast/LabeledActivityNode.cs
new file mode 100644
index 000000000..a29b1f36e
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Ast/LabeledActivityNode.cs
@@ -0,0 +1,20 @@
+using JetBrains.Annotations;
+
+namespace Elsa.Dsl.ElsaScript.Ast;
+
+///
+/// Represents a labeled activity in a flowchart (node declaration).
+///
+[UsedImplicitly]
+public class LabeledActivityNode : AstNode
+{
+ ///
+ /// The label (node name).
+ ///
+ public string Label { get; set; } = string.Empty;
+
+ ///
+ /// The activity statement (can be ActivityInvocationNode, BlockNode, etc.).
+ ///
+ public StatementNode Activity { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Ast/ListenNode.cs b/src/modules/Elsa.Dsl.ElsaScript/Ast/ListenNode.cs
new file mode 100644
index 000000000..0d2e355bd
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Ast/ListenNode.cs
@@ -0,0 +1,12 @@
+namespace Elsa.Dsl.ElsaScript.Ast;
+
+///
+/// Represents a listen statement (for triggers).
+///
+public class ListenNode : StatementNode
+{
+ ///
+ /// The activity invocation for the trigger.
+ ///
+ public ActivityInvocationNode Activity { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Ast/ProgramNode.cs b/src/modules/Elsa.Dsl.ElsaScript/Ast/ProgramNode.cs
new file mode 100644
index 000000000..8c0190eea
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Ast/ProgramNode.cs
@@ -0,0 +1,19 @@
+namespace Elsa.Dsl.ElsaScript.Ast;
+
+///
+/// Represents the root of an ElsaScript program, which can contain:
+/// - Global use statements
+/// - One or more workflow declarations
+///
+public class ProgramNode : AstNode
+{
+ ///
+ /// Global use statements (imports and expression language settings) that apply to all workflows.
+ ///
+ public List GlobalUseStatements { get; set; } = [];
+
+ ///
+ /// Workflow declarations in this program.
+ ///
+ public List Workflows { get; set; } = [];
+}
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Ast/StatementNode.cs b/src/modules/Elsa.Dsl.ElsaScript/Ast/StatementNode.cs
new file mode 100644
index 000000000..3f35976d5
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Ast/StatementNode.cs
@@ -0,0 +1,8 @@
+namespace Elsa.Dsl.ElsaScript.Ast;
+
+///
+/// Base class for statement nodes.
+///
+public abstract class StatementNode : AstNode
+{
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Ast/SwitchCaseNode.cs b/src/modules/Elsa.Dsl.ElsaScript/Ast/SwitchCaseNode.cs
new file mode 100644
index 000000000..ca44d7afc
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Ast/SwitchCaseNode.cs
@@ -0,0 +1,17 @@
+namespace Elsa.Dsl.ElsaScript.Ast;
+
+///
+/// Represents a switch case.
+///
+public class SwitchCaseNode : AstNode
+{
+ ///
+ /// The case value.
+ ///
+ public ExpressionNode Value { get; set; } = null!;
+
+ ///
+ /// The case body.
+ ///
+ public StatementNode Body { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Ast/SwitchNode.cs b/src/modules/Elsa.Dsl.ElsaScript/Ast/SwitchNode.cs
new file mode 100644
index 000000000..efcf5f831
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Ast/SwitchNode.cs
@@ -0,0 +1,22 @@
+namespace Elsa.Dsl.ElsaScript.Ast;
+
+///
+/// Represents a switch statement.
+///
+public class SwitchNode : StatementNode
+{
+ ///
+ /// The switch expression.
+ ///
+ public ExpressionNode Expression { get; set; } = null!;
+
+ ///
+ /// The cases.
+ ///
+ public List Cases { get; set; } = [];
+
+ ///
+ /// The optional default case.
+ ///
+ public StatementNode? Default { get; set; }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Ast/UseNode.cs b/src/modules/Elsa.Dsl.ElsaScript/Ast/UseNode.cs
new file mode 100644
index 000000000..517393b85
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Ast/UseNode.cs
@@ -0,0 +1,33 @@
+namespace Elsa.Dsl.ElsaScript.Ast;
+
+///
+/// Represents a use statement (import or expression language configuration).
+///
+public class UseNode : AstNode
+{
+ ///
+ /// The type of use statement (e.g., "expressions", namespace import).
+ ///
+ public UseType Type { get; set; }
+
+ ///
+ /// The value (e.g., "js", "cs", or a namespace).
+ ///
+ public string Value { get; set; } = string.Empty;
+}
+
+///
+/// The type of use statement.
+///
+public enum UseType
+{
+ ///
+ /// Expression language configuration (e.g., "use expressions js;").
+ ///
+ Expressions,
+
+ ///
+ /// Namespace import (e.g., "use Elsa.Activities.Console;").
+ ///
+ Namespace
+}
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Ast/VariableDeclarationNode.cs b/src/modules/Elsa.Dsl.ElsaScript/Ast/VariableDeclarationNode.cs
new file mode 100644
index 000000000..2c81d3e39
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Ast/VariableDeclarationNode.cs
@@ -0,0 +1,22 @@
+namespace Elsa.Dsl.ElsaScript.Ast;
+
+///
+/// Represents a variable declaration.
+///
+public class VariableDeclarationNode : StatementNode
+{
+ ///
+ /// The variable kind (var, let, const).
+ ///
+ public VariableKind Kind { get; set; }
+
+ ///
+ /// The variable name.
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// The initial value expression.
+ ///
+ public ExpressionNode? Value { get; set; }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Ast/VariableKind.cs b/src/modules/Elsa.Dsl.ElsaScript/Ast/VariableKind.cs
new file mode 100644
index 000000000..87119334b
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Ast/VariableKind.cs
@@ -0,0 +1,10 @@
+namespace Elsa.Dsl.ElsaScript.Ast;
+
+///
+/// The kind of variable declaration.
+///
+public enum VariableKind
+{
+ Var,
+ Const
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Ast/WhileNode.cs b/src/modules/Elsa.Dsl.ElsaScript/Ast/WhileNode.cs
new file mode 100644
index 000000000..4005a5cd5
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Ast/WhileNode.cs
@@ -0,0 +1,17 @@
+namespace Elsa.Dsl.ElsaScript.Ast;
+
+///
+/// Represents a while statement.
+///
+public class WhileNode : StatementNode
+{
+ ///
+ /// The condition expression.
+ ///
+ public ExpressionNode Condition { get; set; } = null!;
+
+ ///
+ /// The body statement.
+ ///
+ public StatementNode Body { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Ast/WorkflowNode.cs b/src/modules/Elsa.Dsl.ElsaScript/Ast/WorkflowNode.cs
new file mode 100644
index 000000000..7019d904d
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Ast/WorkflowNode.cs
@@ -0,0 +1,29 @@
+namespace Elsa.Dsl.ElsaScript.Ast;
+
+///
+/// Represents a workflow definition in ElsaScript.
+///
+public class WorkflowNode : AstNode
+{
+ ///
+ /// The workflow identifier used in the DSL (the string after "workflow" keyword).
+ /// This also serves as the default DefinitionId if not explicitly specified in metadata.
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// Workflow metadata properties (DisplayName, Description, DefinitionId, Version, etc.)
+ /// These are specified in the optional parenthesized argument list after the workflow id.
+ ///
+ public Dictionary Metadata { get; set; } = new();
+
+ ///
+ /// The use statements (imports and expression language settings) at workflow scope.
+ ///
+ public List UseStatements { get; set; } = [];
+
+ ///
+ /// The body of the workflow (a sequence of statements).
+ ///
+ public List Body { get; set; } = [];
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Dsl.ElsaScript/Compiler/ElsaScriptCompiler.cs b/src/modules/Elsa.Dsl.ElsaScript/Compiler/ElsaScriptCompiler.cs
new file mode 100644
index 000000000..8cb52e32f
--- /dev/null
+++ b/src/modules/Elsa.Dsl.ElsaScript/Compiler/ElsaScriptCompiler.cs
@@ -0,0 +1,600 @@
+using Elsa.Dsl.ElsaScript.Ast;
+using Elsa.Dsl.ElsaScript.Contracts;
+using Elsa.Dsl.ElsaScript.Helpers;
+using Elsa.Expressions.Models;
+using Elsa.Workflows;
+using Elsa.Workflows.Activities;
+using Elsa.Workflows.Activities.Flowchart.Models;
+using Elsa.Workflows.Memory;
+using Elsa.Workflows.Models;
+
+namespace Elsa.Dsl.ElsaScript.Compiler;
+
+///
+/// Compiles an ElsaScript AST into Elsa workflows.
+///
+public class ElsaScriptCompiler(IActivityRegistryLookupService activityRegistryLookupService, IElsaScriptParser parser) : IElsaScriptCompiler
+{
+ private static readonly Dictionary LanguageMappings = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["js"] = "JavaScript",
+ ["cs"] = "CSharp",
+ ["py"] = "Python",
+ ["liquid"] = "Liquid"
+ };
+
+ private string _defaultExpressionLanguage = "JavaScript";
+ private readonly Dictionary _variables = new();
+
+ ///
+ public async Task CompileAsync(string source, CancellationToken cancellationToken = default)
+ {
+ var programNode = parser.Parse(source);
+ return await CompileAsync(programNode, cancellationToken);
+ }
+
+ ///
+ public async Task CompileAsync(ProgramNode programNode, CancellationToken cancellationToken = default)
+ {
+ // Get the single workflow (enforced by parser)
+ var workflowNode = programNode.Workflows.First();
+
+ // Merge global use statements with workflow-level ones
+ // Create a temporary workflow node with merged use statements
+ var mergedWorkflowNode = new WorkflowNode
+ {
+ Id = workflowNode.Id,
+ Metadata = workflowNode.Metadata,
+ UseStatements = [..programNode.GlobalUseStatements, ..workflowNode.UseStatements],
+ Body = workflowNode.Body
+ };
+
+ return await CompileWorkflowNodeAsync(mergedWorkflowNode, cancellationToken);
+ }
+
+ private async Task CompileWorkflowNodeAsync(WorkflowNode workflowNode, CancellationToken cancellationToken = default)
+ {
+ _variables.Clear();
+ _defaultExpressionLanguage = "JavaScript";
+
+ // Process use statements (workflow-level overrides global)
+ foreach (var useNode in workflowNode.UseStatements)
+ {
+ if (useNode.Type == UseType.Expressions)
+ {
+ _defaultExpressionLanguage = MapLanguageName(useNode.Value);
+ }
+ }
+
+ // Compile body statements
+ var activities = new List();
+ foreach (var statement in workflowNode.Body)
+ {
+ var activity = await CompileStatementAsync(statement, cancellationToken);
+ if (activity != null)
+ {
+ activities.Add(activity);
+ }
+ }
+
+ // Create the root activity (Sequence containing all statements)
+ var root = activities.Count == 1
+ ? activities[0]
+ : new Sequence
+ {
+ Activities = activities
+ };
+
+ // Extract metadata with defaults
+ var definitionId = GetMetadataValue(workflowNode.Metadata, "DefinitionId") ?? workflowNode.Id;
+ var displayName = GetMetadataValue(workflowNode.Metadata, "DisplayName") ?? workflowNode.Id;
+ var description = GetMetadataValue(workflowNode.Metadata, "Description") ?? string.Empty;
+ var definitionVersionId = GetMetadataValue(workflowNode.Metadata, "DefinitionVersionId") ?? $"{definitionId}-v1";
+ var version = GetMetadataValueOrDefault(workflowNode.Metadata, "Version", 1);
+ var usableAsActivity = GetMetadataValue(workflowNode.Metadata, "UsableAsActivity");
+
+ // Create the workflow
+ var workflow = new Workflow
+ {
+ Name = displayName,
+ Identity = new WorkflowIdentity(definitionId, version, definitionVersionId, null),
+ WorkflowMetadata = new(displayName, description, ToolVersion: new("3.6.0")),
+ Root = root,
+ Variables = _variables.Values.ToList(),
+ Options = new()
+ {
+ UsableAsActivity = usableAsActivity
+ }
+ };
+
+ return workflow;
+ }
+
+ private T? GetMetadataValue(Dictionary metadata, string key) =>
+ metadata.TryGetValue(key, out var value) ? ConvertValue(value, default) : default;
+
+ private T GetMetadataValueOrDefault(Dictionary metadata, string key, T defaultValue) =>
+ metadata.TryGetValue(key, out var value) ? ConvertValue(value, defaultValue) : defaultValue;
+
+ private static T? ConvertValue(object value, T? defaultValue)
+ {
+ // Handle direct type match
+ if (value is T typedValue)
+ return typedValue;
+
+ // Try to convert
+ try
+ {
+ var targetType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
+ return (T)Convert.ChangeType(value, targetType);
+ }
+ catch (Exception)
+ {
+ // Return default value if conversion fails
+ return defaultValue;
+ }
+ }
+
+ private async Task CompileStatementAsync(StatementNode statement, CancellationToken cancellationToken = default)
+ {
+ return statement switch
+ {
+ VariableDeclarationNode varDecl => CompileVariableDeclaration(varDecl),
+ ActivityInvocationNode actInv => await CompileActivityInvocationAsync(actInv, cancellationToken),
+ BlockNode block => await CompileBlockAsync(block, cancellationToken),
+ IfNode ifNode => await CompileIfAsync(ifNode, cancellationToken),
+ ForEachNode forEach => await CompileForEachAsync(forEach, cancellationToken),
+ ForNode forNode => await CompileForAsync(forNode, cancellationToken),
+ WhileNode whileNode => await CompileWhileAsync(whileNode, cancellationToken),
+ SwitchNode switchNode => await CompileSwitchAsync(switchNode, cancellationToken),
+ FlowchartNode flowchart => await CompileFlowchartAsync(flowchart, cancellationToken),
+ ListenNode listen => await CompileListenAsync(listen, cancellationToken),
+ _ => throw new NotSupportedException($"Statement type {statement.GetType().Name} is not supported")
+ };
+ }
+
+ private IActivity? CompileVariableDeclaration(VariableDeclarationNode varDecl)
+ {
+ // Create and register the variable
+ var initialValue = varDecl.Value != null ? EvaluateConstantExpression(varDecl.Value) : null;
+ var variable = new Variable(varDecl.Name, initialValue);
+ _variables[varDecl.Name] = variable;
+
+ // Variable declarations don't produce activities themselves
+ return null;
+ }
+
+ private async Task CompileActivityInvocationAsync(ActivityInvocationNode actInv, CancellationToken cancellationToken = default)
+ {
+ // Try to find the activity type by name - try several strategies
+ var activityDescriptor = await activityRegistryLookupService.FindAsync(actInv.ActivityName);
+
+ // If not found, try with "Elsa." prefix
+ if (activityDescriptor == null)
+ {
+ activityDescriptor = await activityRegistryLookupService.FindAsync($"Elsa.{actInv.ActivityName}");
+ }
+
+ // If still not found, search by descriptor name
+ if (activityDescriptor == null)
+ {
+ activityDescriptor = await activityRegistryLookupService.FindAsync(d => d.Name == actInv.ActivityName);
+ }
+
+ if (activityDescriptor == null)
+ {
+ throw new InvalidOperationException($"Activity '{actInv.ActivityName}' not found in registry");
+ }
+
+ var activityType = activityDescriptor.ClrType;
+
+ // Separate named and positional arguments
+ var namedArgs = actInv.Arguments.Where(a => a.Name != null).ToList();
+ var positionalArgs = actInv.Arguments.Where(a => a.Name == null).ToList();
+
+ IActivity activity;
+
+ // If we have positional arguments, try to find a matching constructor
+ if (positionalArgs.Any())
+ {
+ activity = InstantiateActivityUsingConstructor(activityType, positionalArgs);
+ }
+ else
+ {
+ // No positional arguments, use default constructor
+ var activityConstructorContext = new ActivityConstructorContext(activityDescriptor, ActivityActivator.Create);
+ activity = activityDescriptor.Constructor(activityConstructorContext);
+ }
+
+ // Set named argument properties
+ foreach (var arg in namedArgs)
+ {
+ var property = activityType.GetProperty(arg.Name!);
+
+ if (property != null)
+ {
+ var value = CompileExpression(arg.Value, property.PropertyType);
+ property.SetValue(activity, value);
+ }
+ else
+ {
+ throw new InvalidOperationException($"Property '{arg.Name}' not found on activity type '{activityType.Name}'");
+ }
+ }
+
+ return activity;
+ }
+
+ private async Task CompileBlockAsync(BlockNode block, CancellationToken cancellationToken = default)
+ {
+ var activities = new List();
+
+ foreach (var statement in block.Statements)
+ {
+ var activity = await CompileStatementAsync(statement, cancellationToken);
+ if (activity != null)
+ {
+ activities.Add(activity);
+ }
+ }
+
+ return new Sequence
+ {
+ Activities = activities
+ };
+ }
+
+ private async Task CompileIfAsync(IfNode ifNode, CancellationToken cancellationToken = default)
+ {
+ var condition = CompileExpressionAsInput(ifNode.Condition);
+ var thenActivity = await CompileStatementAsync(ifNode.Then, cancellationToken);
+ var elseActivity = ifNode.Else != null ? await CompileStatementAsync(ifNode.Else, cancellationToken) : null;
+
+ return new If(condition)
+ {
+ Then = thenActivity,
+ Else = elseActivity
+ };
+ }
+
+ private async Task CompileForEachAsync(ForEachNode forEach, CancellationToken cancellationToken = default)
+ {
+ Variable loopVariable;
+
+ if (forEach.DeclaresVariable)
+ {
+ // Create a new loop variable
+ loopVariable = new Variable