Skip to content

Conversation

@sfmskywalker
Copy link
Member

@sfmskywalker sfmskywalker commented Nov 19, 2025

Description

This PR introduces ElsaScript, a JavaScript-inspired textual Domain Specific Language (DSL) for authoring Elsa 3 workflows.

While the visual designer remains excellent for high-level composition and non-technical users, a textual DSL fills a critical gap for developers who need speed, precision, and better integration with source control workflows.

Motivation & Rationale

Why introduce a textual DSL when we already have JSON/YAML and a visual designer?

  1. Superior Source Control & Code Reviews

    • The Problem: Reviewing changes in large JSON/YAML workflow definitions is difficult. Diffs are often noisy, formatting-dependent, and semantically opaque.
    • The Solution: ElsaScript provides a concise, human-readable format. PR reviews become meaningful because the logic is visible, not buried in serialization syntax. Merge conflicts become standard code conflicts, which are easier to resolve.
  2. Developer Productivity

    • The Problem: For developers, "drag-and-drop" can be slower than typing, especially when defining variables, loops, and complex conditional logic.
    • The Solution: ElsaScript allows developers to author workflows at the speed of typing. Refactoring (e.g., renaming a variable across a workflow) becomes a simple Find & Replace operation.
  3. Runtime Flexibility with "Compiled" Safety

    • ElsaScript compiles directly to the Elsa 3 Workflow object model at runtime. This allows workflows to be loaded dynamically from Blob Storage or databases (like scripts) without requiring a full project recompile/redeployment (unlike C# workflows), while offering a much terser syntax than raw JSON.
  4. Expressive Logic

    • The DSL supports high-level constructs like flowchart, for loops, if/else blocks, and switch statements that map 1:1 to their Activity counterparts. It also supports mixed expression languages (JavaScript, C#, Liquid) within the same file.

Key Features

  • Hybrid Syntax: Familiar C# and JavaScript-like syntax for defining variables (var, const) and control flow.
  • Flowchart Support: First-class support for defining graph-based logic using text (nodes, connections, entry points).
  • Metadata Support: Define workflow properties (DefinitionId, Version, Description) directly in the header.
  • Activity Discovery: Dynamically resolves activities from the registry, supporting both built-in and custom activities.
  • Performance: Includes optimizations for Blob Storage providers to filter files by extension before parsing.

Example

A simple workflow demonstrating loops, mixed expression languages (JS and C#), and variable scoping:

use expressions js;

workflow ForLoopTest 
{
    WriteLine("First loop");

    // Range-based loop with JavaScript expression interpolation
    for (var i = 0 through 10 step 1)
    {
        WriteLine(js => `Step: ${variables.i}`);
    }

    WriteLine("Second loop");

    // Range-based loop reusing variable with C# expression
    for (var i = 0 to 10 step 2)
      WriteLine(cs => return $"Step: {Variables.Get("i")}";);
  
    WriteLine("Done!");
}

This change is Reviewable

sfmskywalker and others added 30 commits June 6, 2025 18:47
…t references

- Updated multiple package versions in `Directory.Packages.props` for better dependency management, including `BenchmarkDotNet`, `FastEndpoints`, and `Microsoft.Extensions.Http.Resilience`.
- Minor version upgrade for `System.Formats.Asn1` in `_build.csproj`.
- Replaced project reference to `Elsa.csproj` with `Elsa.IO.Http.csproj` in `Elsa.ServerAndStudio.Web.csproj`, enhancing modularity.
- Added new using directive for `Elsa.IO.Http.Features` in `Program.cs` to support new HTTP functionalities.
These changes indicate that the associated projects or dependencies are no longer needed or have been replaced by other components in the solution.
* Update RawStringContent encoding in JsonContentFactory

Modified the instantiation of `RawStringContent` to use a
new `UTF8Encoding` instance with `encoderShouldEmitUTF8Identifier`
set to `false`, affecting the handling of the UTF-8 byte order
mark (BOM) in serialized JSON content. Fixes a bug with content length being different than expected.

* Refactor JsonContentFactory to reuse UTF8Encoding

Introduced a private static readonly field `_utf8Encoding` in the `JsonContentFactory` class to improve code readability and performance. This change replaces the instantiation of `UTF8Encoding` in the `CreateHttpContent` method, allowing for the reuse of the same encoding instance.

---------

Co-authored-by: Max Brooks <[email protected]>
* Enhance thread safety with ConcurrentDictionary usage

Replaced `IDictionary` with `ConcurrentDictionary` for
both `_scheduledTasks` and `_scheduledTaskKeys` to
improve thread safety in a multi-threaded environment.

Updated methods `RegisterScheduledTask`,
`RemoveScheduledTask`, and `RemoveScheduledTasks` to
utilize the `Remove` method of `ConcurrentDictionary`,
ensuring safe and efficient removal of scheduled tasks.

* Refactor task registration and removal logic

Updated `RegisterScheduledTask` to use `AddOrUpdate` for streamlined task management. This change simplifies the addition and updating of scheduled tasks by consolidating logic into a single operation. Introduced `RemoveScheduledTask` method to handle task removal by name, improving code organization and clarity.

* Improve task removal handling in LocalScheduler

Modified the `LocalScheduler` class to enhance the removal process of scheduled tasks from the `_scheduledTaskKeys` collection. The removal operation now captures the result in a variable and includes a conditional check to log a warning if the task was not found, improving error handling and debugging capabilities.

* Refactor task removal in LocalScheduler

Updated the removal process for scheduled tasks in `_scheduledTasks`.
The new implementation collects all corresponding keys and attempts to remove them individually, logging warnings for any failures. This enhances error handling and provides better debugging information.

---------

Co-authored-by: Max Brooks <[email protected]>
- Deleted several project references from `Elsa.sln` to clean up the solution.
- Updated `Directory.Packages.props` for consistency and alignment with the latest package versions.
- Removed `ActivityFactory` and its related interfaces and extensions.
- Introduced `ActivityActivator` for handling activity creation.
- Extended AST with support for comprehensive workflow structures:
  - Added nodes for flowcharts, if/else, loops, and variable declarations.
- Updated `IElsaScriptCompiler` to use asynchronous methods.
- Expanded `ElsaScriptParser` to simplify syntax for `UseNode` and argument parsing.
- Adjusted compiler and parser for compatibility with new workflow AST model.
…r tests

- Updated method names in `CompilerTests` and `ParserTests` for better readability and description of test intent.
- Added tests for compiler and parser:
  - Support for workflows without the `workflow` keyword.
…e a tokenizer

- Added `TokenizeStatements` method to split source into statements for enhanced parsing accuracy.
- Updated logic to process statements instead of raw lines, reducing parsing complexity and improving reliability.
- Improved handling of workflow and statement parsing, including edge cases with braces, parentheses, and string literals.
- Added the `Elsa.WorkflowProviders.BlobStorage.ElsaScript` module to enable ElsaScript-based workflow definitions for BlobStorage.
- Implemented `ElsaScriptBlobWorkflowFormatHandler` for parsing ElsaScript workflows stored in BlobStorage.
- Extended `ElsaScriptParser` to leverage Parlot for improved DSL parsing.
- Introduced `IBlobWorkflowFormatHandler` to centralize workflow format handling and parsing.
- Updated `Elsa.Server.Web` to reference the new module and include an ElsaScript "Hello World" example workflow.
…ndling

- Changed `ElsaScriptCompiler` service registration from `Singleton` to `Scoped` for better dependency management.
- Enhanced the "Hello World" example workflow and added `CopyToOutputDirectory` configuration.
- Removed unused namespaces and adjusted references in multiple projects to improve maintainability.
- Updated logging levels in `appsettings.json` to reduce unnecessary debug output.
- Improved `PolymorphicObjectConverter` by removing redundant dependencies.
- Added missing references to enhance feature support and ensure compatibility.
…aScriptCompiler`

- Added support for positional arguments with constructor matching logic.
- Refactored `InstantiateActivityUsingConstructor` to enhance activity creation.
- Updated `ActivityDescriptor` and related types to include `ClrType` for streamlined activity resolution.
- Simplified `TypedActivityProvider` by annotating it with `[UsedImplicitly]`.
- Adjusted `ElsaScriptParser` to remove unnecessary options from string literal definitions.
- Deleted `elsa-studio.yml` workflow as it's no longer used.
- Updated `Elsa.sln` to remove reference to the deleted workflow.
- Changed `base_version` in `packages.yml` from `3.7.0` to `3.6.0`.
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +55 to +60
// Process use statements (workflow-level overrides global)
foreach (var useNode in workflowNode.UseStatements)
{
if (useNode.Type == UseType.Expressions)
{
_defaultExpressionLanguage = MapLanguageName(useNode.Value);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reset default expression language per compilation

ElsaScriptCompiler stores the default expression language in _defaultExpressionLanguage and mutates it when a use expressions directive is parsed, but CompileWorkflowNodeAsync never reinitializes the field for a new compilation. Because the compiler is registered as a scoped service and reused, compiling a workflow after one that set use expressions liquid will keep Liquid as the default even if the later workflow omits that directive, so unqualified expressions get compiled in the wrong language. Reset the default before processing each workflow to avoid leaking state across compilations.

Useful? React with 👍 / 👎.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces ElsaScript, a JavaScript-inspired textual DSL for authoring Elsa 3 workflows, providing a code-centric alternative to C# WorkflowBuilder APIs and JSON workflow definitions.

Key changes include:

  • New ElsaScript parser and compiler using Parlot for robust parsing
  • AST-based workflow compilation with support for variables, control flow, and flowcharts
  • Integration with BlobStorage provider for .elsa file support
  • Database schema updates to support symmetric round-trip preservation of original source
  • Refactored activity instantiation to remove dependency on IActivityFactory

Reviewed Changes

Copilot reviewed 105 out of 108 changed files in this pull request and generated 33 comments.

Show a summary per file
File Description
src/modules/Elsa.Dsl.ElsaScript/ New module implementing ElsaScript parser, compiler, AST nodes, and materializer
src/modules/Elsa.WorkflowProviders.BlobStorage.ElsaScript/ BlobStorage integration for ElsaScript workflows
src/modules/Elsa.Workflows.Core/Models/ActivityConstructorContext.cs Refactored from interface-based to factory-based activity instantiation
src/modules/Elsa.Workflows.Management/Entities/WorkflowDefinition.cs Added OriginalSource field for symmetric round-trip support
test/integration/Elsa.Dsl.ElsaScript.IntegrationTests/ Comprehensive parser and compiler integration tests
Database migrations Added OriginalSource column across all EF Core providers
src/modules/Elsa.Scheduling/Services/LocalScheduler.cs Fixed race conditions by using ConcurrentDictionary
Files not reviewed (2)
  • src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Management/20251116182628_V3_6.Designer.cs: Language not supported
  • src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Management/20251116182711_V3_6.Designer.cs: Language not supported
Comments suppressed due to low confidence (4)

src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Management/20251116182753_V3_6.cs:30

  • The migration adds the OriginalSource column, then performs ALTER TABLE operations, and finally drops the OriginalSource column in the same Up() method. This will remove the column that was just added. The DropColumn should only be in the Down() method for rollback.
    src/modules/Elsa.Workflows.Management/Extensions/ActivityFactoryExtensions.cs:1
  • The entire ActivityFactoryExtensions.cs file has been deleted, but references to this extension method may still exist elsewhere in the codebase if it was used. Ensure that all usages of the Create<T>() extension method have been updated to use the new pattern.
    test-flowchart.txt:1
  • Test files test-flowchart.txt and test-simple-flowchart.elsa appear to be temporary test artifacts that should not be committed to the repository root. Consider moving them to a test fixtures directory or adding them to .gitignore.
    src/modules/Elsa.Workflows.Core/Models/ActivityConstructorContext.cs:62
  • Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.

Comment on lines +22 to +26
name: "OriginalSource",
schema: _schema.Schema,
table: "WorkflowDefinitions",
type: "NCLOB",
nullable: true);
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using NCLOB for OriginalSource in Oracle may cause performance issues for large workflow definitions. The ModelSnapshot shows NVARCHAR2(2000) instead. Consider using CLOB or ensure consistency between migration and snapshot, or document the size limitation if NVARCHAR2(2000) is intentional.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback. Use the recommended solution, keeping in mind that OriginalSource may exceed the 2000 character limit, given that it may store large workflow definitions in JSON format.

Comment on lines 64 to 66
var removed = _scheduledTaskKeys.TryRemove(existingScheduledTask, out ICollection<string>? _);
if (!removed)
System.Diagnostics.Debug.WriteLine($"[LocalScheduler] Warning: Tried to remove scheduled task keys for an existing scheduled task, but it was not present in _scheduledTaskKeys.");
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using System.Diagnostics.Debug.WriteLine for production diagnostics is not recommended. This output will only appear in debug builds and won't be visible in production logs. Use the ILogger infrastructure instead for consistent logging across environments.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback. Find all occurrences of System.Diagnostics.Debug.WriteLine and replace them appropriately with the ILogger infrastructure.

Comment on lines +29 to +38
if (p.HasDefaultValue)
{
args[i] = p.DefaultValue;
}
else
{
args[i] = p.ParameterType.IsValueType
? Activator.CreateInstance(p.ParameterType)
: null;
}
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.

Copilot uses AI. Check for mistakes.
Comment on lines +495 to +503
if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Input<>))
{
// Extract the T from Input<T>
innerType = targetType.GetGenericArguments()[0];
}
else
{
innerType = targetType;
}
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.

Copilot uses AI. Check for mistakes.
Comment on lines 131 to 140
if (materializedWorkflow.MaterializerName == "Json")
{
stringData = _activitySerializer.Serialize(workflow.Root);
}
else
{
// For new formats (ElsaScript, YAML, etc.), only OriginalSource is needed
// StringData can be null as these materializers only use OriginalSource
stringData = null;
}
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.

Copilot uses AI. Check for mistakes.
sfmskywalker and others added 8 commits November 19, 2025 20:40
…rts in `LocalScheduler`

- Cleaned up unnecessary using directives to improve code readability and maintainability.
- Minor whitespace adjustment for consistent formatting.
- Updated exception handling in `ElsaScriptBlobWorkflowFormatHandler` and `JsonBlobWorkflowFormatHandler` to gracefully catch and log all exceptions during workflow parsing.
- Adjusted comments to clarify behavior for invalid user-provided files, ensuring the workflow loading process is not disrupted.
…or improved file filtering

- Added `SupportedExtensions` property to all blob format handlers to optimize blob storage browsing.
- Simplified `CanHandle` logic by removing extension checks, leveraging `SupportedExtensions` for initial filtering.
- Updated comments for clarity and consistency across handlers.
…gData` assignment logic and improve readability
Copy link
Contributor

Copilot AI commented Nov 19, 2025

@sfmskywalker I've opened a new pull request, #7079, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Contributor

Copilot AI commented Nov 19, 2025

@sfmskywalker I've opened a new pull request, #7080, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Contributor

Copilot AI commented Nov 19, 2025

@sfmskywalker I've opened a new pull request, #7081, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Contributor

Copilot AI commented Nov 19, 2025

@sfmskywalker I've opened a new pull request, #7082, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Contributor

Copilot AI commented Nov 19, 2025

@sfmskywalker I've opened a new pull request, #7083, to work on those changes. Once the pull request is ready, I'll request review from you.

sfmskywalker and others added 7 commits November 19, 2025 21:41
…prove language mapping, and enhance asynchronous flowchart compilation
* Initial plan

* Fix ParseError formatting to use Message and Position properties

Co-authored-by: sfmskywalker <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: sfmskywalker <[email protected]>
…7083)

* Initial plan

* Replace 'as' casts with direct casts in ParserTests for better null safety

Co-authored-by: sfmskywalker <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: sfmskywalker <[email protected]>
#7079)

* Initial plan

* Fix Oracle OriginalSource and StringData column types to handle large data

Co-authored-by: sfmskywalker <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: sfmskywalker <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants