Find a file
Marko Lahma 7328a0f14d
Adopt the Jint 4.15 host-integration surface: lazy type globals, enum names, and register-what-is-referenced (#7895)
* chore(javascript): update Jint to 4.15.3 and stop blocking on promises

`Engine.Evaluate(...).UnwrapIfPromise()` blocks the calling thread while the
engine's event loop drains, which is exactly the wrong thing to do inside an
`async` method — an expression that awaits a .NET `Task`, such as one calling
`getSecret()`, held a thread pool thread for the duration of the I/O.
`Engine.EvaluateAsync` awaits the returned promise instead, and takes the
cancellation token while it is at it.

The Jint version is moved from 4.4.2 to 4.15.3. `EvaluateAsync` arrived in
4.14.0, but the pin lands past 4.15.2 deliberately: once expressions genuinely
suspend and resume instead of draining the event loop on the calling thread,
they exercise the async suspension machinery 4.15.2 corrected — an `await` on a
right-hand side no longer stores the suspension sentinel, async generators and
`for await...of` preserve loop iteration state across a suspension, and a
suspension node is unwrapped correctly. Shipping the non-blocking change on an
earlier 4.14/4.15 would enable exactly the code paths those releases fixed.

One default changed along the way: since 4.14 `Interop.ArrayConversion` defaults
to `LiveView`, so a CLR array reaches script as a live view over the original
array rather than as a copy. That is observable — a script that sorts an array
would now reorder the workflow's own array, and the value round-trips back as
its original element type rather than as `object[]`. The evaluator therefore
pins the previous `Copy` behaviour so the upgrade is not a behavioural change;
hosts that prefer the live view can opt in through
`JintOptions.ConfigureEngineOptions`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179sA2T7HuRfRfSc2JirFik

* test(javascript): pin the array copy lane and adopt JsString.Create

The array audit. The parent commit fixes `ArrayConversion` to `Copy`, because
4.14 changed the default to `LiveView` and the two differ in behaviour a script
can observe. The existing tests assert what a copy *produces*, which a live view
also satisfies for a value nothing mutates; asking the engine how many
conversions of each kind it performed (4.15.1's interop conversion counters)
pins the lane itself. The second assertion is the more interesting one: an
ordinary evaluation converts no CLR array at all, because Elsa converts
collection-valued variables itself in `ObjectConverterHelper` long before Jint's
array lane could see them. That makes the `ArrayConversion` setting a narrow
compatibility pin rather than something every evaluation depends on.

`JsString.Create` (public since Jint 4.15.3) is adopted in
`JsonElementConverter`, where the string case was the only one still routed
through `JsValue.FromObject` — re-entering the whole conversion pipeline, the
registered object converters and this one included, to arrive at the same call
the number and boolean cases beside it already make directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uV6H9cTntzsoKiaJRBn4f

* perf(javascript): register .NET type globals lazily

A fresh Jint engine is built for every expression evaluation, and roughly twenty
.NET types are registered on it before the expression runs: the common types
(`DateTime`, `Guid`, `TimeSpan`, …) plus every non-primitive workflow variable
descriptor type. Each registration builds a `TypeReference`, which describes the
type through reflection. The overwhelming majority of expressions reference none
of them.

`Options.AddLazyGlobal` installs a global whose value is produced by a factory
the first time a script reads the name. Both type registration handlers now run
on `CreatingJavaScriptEngine` — which already carries the `Jint.Options` being
built — and register through it, so a type is only described if an expression
actually mentions it. A script that uses `Guid.NewGuid()` still sees `Guid`; a
script that uses none of them pays for none of them.

Types whose name cannot be written as a JavaScript identifier are skipped while
we are here, since no script can reach them. That covers constructed generic
types (``IDictionary`2``, which two different variable descriptors both claimed)
and array types (`Byte[]`).

Moving the registrations to engine construction has a consequence worth pinning
beyond the ordering: a global installed by the host through `configureEngine` is
no longer overwritten by the built-in registration of the same name. The lazy
global for `Guid` is already installed when that callback runs, and
`Engine.SetValue` goes through `[[Set]]` on the global object, which reads the
current value — running the factory once and discarding the result — before
replacing the descriptor. The end state is the host value, but only the test
says so.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uV6H9cTntzsoKiaJRBn4f

* perf(javascript): register the common functions lazily

The same argument as the type globals, applied to the other bulk registration a
fresh engine pays for. `ConfigureEngineWithCommonFunctions` installs
twenty-seven functions on every engine — `getVariable`, `toJson`, `newGuid`, the
base64 helpers, the deprecated GUID pair — and each `engine.SetValue(name,
Delegate)` builds an interop function wrapper around the delegate: a JavaScript
function object, plus the signature metadata and invoker lookups Jint resolves
per delegate type. An expression such as `variables.Foo` or a comparison calls
none of them.

This was recorded in the PR as deferred, because a lazy version had to keep the
`NonEnumerable` flag `SetValue(string, Delegate)` applies — otherwise the
functions would start appearing in `Object.keys(globalThis)`, which is
observable. Jint 4.15.3's `Engine.Advanced.AddLazyGlobal` takes a `PropertyFlag`
and is the post-construction counterpart of the options-time API used for the
type globals, so the flag is passed explicitly and the port is a line per
function. The CLR delegate is now created inside the factory as well, so a
function nothing reads costs one closure rather than a closure, a delegate and a
wrapper.

The laziness is invisible, and the tests say so rather than leaving it implied.
`AddLazyGlobal` installs the property itself eagerly and defers only its value,
so `in`, `hasOwnProperty` and `Object.getOwnPropertyNames` answer immediately
without materialising anything; `Object.keys(globalThis)` still omits them; the
descriptor still reports writable, non-enumerable and configurable; two reads of
a function are the same value, which is what says the factory ran once and the
result was stored rather than recomputed per read; and a script can still
overwrite one.

Not separately measured. The measured table in the PR predates this commit and
was not re-run for it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uV6H9cTntzsoKiaJRBn4f

* refactor(javascript): let Jint expose enums as names and type its converters

`EnumToStringConverter` turned every CLR enum crossing into JavaScript into
`Enum.ToString()`. Jint 4.15 does that with `Interop.EnumConversion`, so the
converter goes away.

It is not only fewer moving parts. The converter could only see values that
crossed the interop boundary, and a constant read off a registered enum type
does not: `LogPersistenceMode.Include` came back as the underlying number while
the same value held in a workflow variable came back as `"Include"`, so
`mode === LogPersistenceMode.Include` was always false. The built-in switch
covers both directions and they now agree. Values written back to the CLR keep
accepting the member name and the number, as they did before.

That fix changes what an expression using a constant *numerically* produces, and
the hazard worth calling out is persisted state: a workflow variable holding a
number written from a constant before the upgrade no longer compares equal to
that constant after it, which reaches in-flight and resumed instances rather
than only new ones. Typed conversions hold in both directions, so activity
inputs and typed variable reads are unaffected. Recorded in the 3.8.0 changelog.

The two remaining converters register through the overload that declares the CLR
types they handle. A converter that does not declare them has to be offered
every value crossing the boundary, which costs the engine its compiled
member-read and method-invoker lanes for every wrapped .NET object; declaring
`byte[]` and `JsonElement` keeps those lanes for everything that cannot produce
one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uV6H9cTntzsoKiaJRBn4f

* perf(javascript): register only the accessors an expression names

Every evaluation registers a getter and a setter for each variable in scope, a
getter for each workflow input, and a getter for each (activity, output) pair in
the enclosing container. The last of those is the expensive one: naming those
accessors walks every node of the workflow and resolves each against the
activity registry, so the cost of setting up an engine grows with the size of
the workflow rather than the size of the expression. Almost no expression uses
any of them.

Jint reports the free identifiers of a prepared program through
`Prepared<T>.ReferencedGlobals`, which is exactly the question being asked here:
an identifier the expression never mentions cannot be read from it. The
evaluator now prepares the script before configuring the engine — the parse was
already cached, so this only reorders work — and passes the set on
`EvaluatingJavaScript`. The accessor handler registers a name only if it is in
the set, and skips the workflow walk entirely when no identifier of the
`get{Output}From{Activity}` shape appears.

The filter is sound for an expression that names an accessor the way one is
meant to be named, and not for one that builds or reaches a name at run time.
Four such forms are detectable and each turns the filter off. A direct `eval`
call is reported as `HasDirectEvalCall`. An *indirect* `eval` call and the
`Function` constructor are deliberately not flagged as direct calls — Jint
reports the identifiers `eval` and `Function` in the set instead, and says so,
because that is the signal a host is meant to act on. Missing that signal is not
theoretical: `new Function('return getMyVariable()')()` would regress from
working to a `ReferenceError`, since Function-constructed code resolves only
against the global scope, and `var e = eval; e('getMyVariable()')` would fail the
same way. The fourth is a reference to `globalThis`, which reaches a global
without naming it. All four are pinned.

What stays undetectable is reaching the global object without naming it at all —
a sloppy-mode top-level `this`, or `[].constructor.constructor(…)`. An
expression written that way loses the generated accessor but not the data:
`getVariable(name)`, `getInput(name)` and `getOutputFrom(activityId, outputName)`
are always registered and reach the same values.

Note this does not replace the regular expression in
`ConfigureEngineWithVariables`, which extracts the member names in
`variables.Foo`. Those are not free identifiers and Jint deliberately does not
report them; the set only says whether `variables` itself is referenced.

The filtering is invisible from inside a script, which also makes it unprovable
from there, so two of the tests hold on to the engine and assert on the globals
directly.

While here, the cancellation token is registered as an engine constraint.
Passing it to `EvaluateAsync` only covers the awaiting part; Jint's own remarks
say the parameter cannot preempt the synchronous evaluation loop and point at
this constraint, so the token read as more coverage than it delivered. A
cancellation constraint is amortizable, so the interpreter keeps its tight-loop
fast path, and a default token registers nothing. #7891 supersedes this with the
full constraint set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uV6H9cTntzsoKiaJRBn4f

* perf(javascript): build marshalled variables in Jint's shaped representation

`ConvertToJsObject` built the JavaScript object a workflow variable is copied
into one `DefineOwnProperty` call at a time, with an explicit descriptor. That
lands the object in Jint's per-object property dictionary, so every marshalled
variable carries its own descriptors even though sibling variables and the
repeated nested payloads of one document present the same key set.

`JsObject.CreateFromEntries` defines the same writable, enumerable and
configurable properties but builds through the shared-layout path. Both wins
land inside a single evaluation — half the descriptor allocations, since the
explicit descriptor caused a second one inside `ValidateAndApplyPropertyDescriptor`,
and one shared layout across objects presenting the same keys. Nothing carries
across evaluations: layouts are interned per engine and per-node inline caches
live in per-engine handler trees that only engage on a second evaluation on the
same engine, which a fresh-engine-per-evaluation host never reaches.

Reaching the shared layout is silent: `CreateFromEntries` falls back to the
ordinary property dictionary whenever a key or a growth guard says the layout
cannot continue, and the object behaves identically either way. A test now asks
the engine whether the object actually got one. `Engine.Advanced.HasSharedShape`,
added in 4.15.3, is the part of that answer Jint documents as a contract — the
finer-grained `GetObjectRepresentation` names an internal representation that may
be renamed or subdivided in any release — and `CreateFromEntries` is one of its
three documented success cases. The assertion is not vacuous: against the
previous property-by-property build it is false, including for the
`CreateDataProperty` variant in #7892, because that object is created through
`Intrinsics.Object.Construct` rather than built as entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uV6H9cTntzsoKiaJRBn4f

* test(javascript): run the scripting suites with Jint's host-contract verifiers on

Jint has a set of verifiers for the extension points it *trusts* — the ones it
cannot afford to re-check on a hot path, so a violation is otherwise silent.
They used to be compiled out of Release, which made "run your suite against a
Debug Jint" the only way to reach them, and Jint ships Release-only. 4.15.3
moves them behind an AppContext switch read once at type initialization, so a
host that never sets it pays nothing (the JIT folds the guards away) and a test
host can turn them on against the shipped package.

The one Elsa is subject to today is the object-converter type declaration this
PR introduced. Registering a converter with `AddObjectConverter(converter,
handledTypes)` promises the engine the converter produces values only for those
types, and in exchange the compiled interop lanes are kept for every member that
cannot produce one. Nothing links that promise to the converter's own
`TryConvert` switch: a case added there and not added to the registration is
silently skipped on exactly the members the declaration excluded, and honoured
everywhere else. `ByteArrayConverter` and `JsonElementConverter` are consistent
today; this is what would report it if they drifted.

Elsa defines no `ObjectInstance` subclass, so the rest of the verifiers have
nothing to check here yet. They are a standing guard for the day a host handler
or a satellite module adds one.

Wired as a module initializer, because the switch has to be set before the first
use of any Jint type. Duplicated across the three suites that reference
`Elsa.Expressions.JavaScript` rather than shared: `Elsa.Testing.Shared.Integration`
would be the obvious home, but it is a published package, and a module
initializer there would flip a process-wide switch for every external consumer
of it as well. A one-line test pins that the initializer ran, since Elsa
satisfies the contracts it is subject to and nothing else would notice the
checks going away.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uV6H9cTntzsoKiaJRBn4f

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
2026-08-17 01:44:03 +02:00
.agents/skills feat: introduce HTTP webhooks module 2026-08-03 02:08:15 +02:00
.claude/skills [codex] Add live server log streaming diagnostics (#7438) 2026-05-11 00:08:52 +02:00
.config Fix Race Condition Causing Duplicate Trigger Registration in Multi-Engine Environments + Add Concurrency Tests to Prevent Regression (#7131) 2025-12-05 09:17:14 +01:00
.github Fix update wiki workflow action 2026-05-18 20:17:09 +02:00
.nuke Upgrade to NUKE 9.0.4 and fix automatic workflow generation 2025-01-26 19:29:19 +02:00
.specify Merge release/3.8.0 into main 2026-08-14 11:59:35 +02:00
.vscode docs: ratify constitution v1.0.0 (7 principles from codebase analysis) 2026-03-08 15:49:20 +01:00
announcements Skills 2026-06-25 15:19:31 +02:00
build build: bump NuGet.Packaging to 7.9.0 to unbreak the NUKE build on SDK 10.0.400 2026-08-11 23:25:33 +02:00
design chore: apply safe dependency upgrades (#7896) 2026-07-26 21:42:27 +02:00
doc Adopt the Jint 4.15 host-integration surface: lazy type globals, enum names, and register-what-is-referenced (#7895) 2026-08-17 01:44:03 +02:00
docker [codex] Fix console log metadata and type resolution (#7542) 2026-05-30 22:52:01 +02:00
docs Add architecture and practices documentation 2026-08-03 23:46:38 +02:00
gen
scripts [codex] Fix console log metadata and type resolution (#7542) 2026-05-30 22:52:01 +02:00
specs Merge release/3.8.0 into main 2026-08-14 11:59:35 +02:00
src Adopt the Jint 4.15 host-integration surface: lazy type globals, enum names, and register-what-is-referenced (#7895) 2026-08-17 01:44:03 +02:00
test Adopt the Jint 4.15 host-integration surface: lazy type globals, enum names, and register-what-is-referenced (#7895) 2026-08-17 01:44:03 +02:00
.editorconfig Refactor to use target-typed object creation 2025-01-09 10:27:55 +01:00
.gitignore Add external authentication broker 2026-07-24 19:04:26 +02:00
AGENTS.md Add Weaver grounding tools 2026-06-08 15:40:10 +02:00
build.cmd Upgrade to NUKE 8.1 (#5952) 2024-09-12 09:56:06 -07:00
build.ps1 Upgrade to NUKE 8.1 (#5952) 2024-09-12 09:56:06 -07:00
build.sh Upgrade to NUKE 8.1 (#5952) 2024-09-12 09:56:06 -07:00
CLAUDE.md docs: add agent operating principles to AGENTS.md and CLAUDE.md 2026-05-06 12:44:27 +02:00
CONTEXT.md Add output converter support at binding boundaries 2026-07-31 04:10:48 +02:00
CONTRIBUTING.md Enhance CONTRIBUTING.md with feature request guidelines 2026-04-14 11:37:12 +02:00
Directory.Build.props Addresses warnings and enforces null safety (#7050) 2025-11-12 15:53:23 +01:00
Directory.Build.targets Add shell middleware, call‑stack tracking, and workflow reference graph APIs (#7333) 2026-02-28 21:16:04 +01:00
Directory.Packages.props Update Jint to 4.15.3 and stop blocking the calling thread on promises (#7894) 2026-08-17 01:02:04 +02:00
dotnet-install.sh Add ActivityCompleted notification for workflow activity completion (#6675) 2025-05-24 23:01:41 +02:00
Elsa.sln Merge release/3.8.0 into main 2026-08-14 11:59:35 +02:00
Elsa.sln.DotSettings feat: extend shells integration and modular server support (#7399) 2026-04-18 14:33:34 +02:00
icon.png
jordi-delay-bookmark-reply.md Skills 2026-06-25 15:19:31 +02:00
LICENSE
NuGet.Config Merge release/3.8.0 into main 2026-08-14 11:59:35 +02:00
pau-delay-dispatch-response.md Skills 2026-06-25 15:19:31 +02:00
README.md [codex] Fail fast on default JWT signing keys (#7496) 2026-05-20 22:30:49 +02:00
ROADMAP.md docs: refresh roadmap 2026-08-12 00:09:13 +02:00
run-dsl-tests.sh Add Elsa Script DSL (#7076) 2025-11-25 19:57:50 +01:00
test-flowchart.txt Add Elsa Script DSL (#7076) 2025-11-25 19:57:50 +01:00
test-simple-flowchart.elsa Add Elsa Script DSL (#7076) 2025-11-25 19:57:50 +01:00

Elsa Workflows

Elsa Workflows

Elsa 3 Prerelease Nuget (with prereleases) Feedz Docker Image Version (latest semver) Discord Stack Overflow questions Gurubase DeepWiki

For Elsa 2, Click Here

Introduction

Elsa is a powerful workflow library that enables workflow execution within any .NET application. Elsa allows you to define workflows in various ways, including:

  • Writing C# code
  • Using a visual designer
  • Specifying workflows in JSON

Elsa ships with a powerful visual designer

Try with Docker

To give the Elsa Studio + Elsa Server a quick spin, you can run the following command to start the Elsa Docker container:

docker pull elsaworkflows/elsa-server-and-studio-v3:latest
docker run -t -i -e ASPNETCORE_ENVIRONMENT='Development' -e HTTP_PORTS=8080 -e HTTP__BASEURL=http://localhost:13000 -p 13000:8080 elsaworkflows/elsa-server-and-studio-v3:latest

This Docker image is based on a reference ASP.NET application that hosts both the workflow server and designer and is not intended for production use.

For any non-development deployment, inject a secure random JWT signing key through environment variables or a secrets manager instead of using committed appsettings values. For code-first hosts such as Elsa.Server.Web, set Identity__Tokens__SigningKey. For shell-based hosts, set the shell feature key, for example CShells__Shells__Default__Features__Identity__SigningKey.

By default, you can access http://localhost:13000 and log in with:

  Username: admin
  Password: password

TLS and custom certificate authorities

All Elsa Docker images now ship with the operating system's certificate authority bundle baked in at build time. This means you can call public HTTPS endpoints such as https://example.com without any additional configuration.

If you need to trust a private or corporate CA, mount the certificate bundle into the container and reference it via EXTRA_CA_CERT:

docker run \
  -v /path/to/company-ca.crt:/certs/company-ca.crt:ro \
  -e EXTRA_CA_CERT=/certs/company-ca.crt \
  elsaworkflows/elsa-server-and-studio-v3:latest

On startup, the container copies the certificate into /usr/local/share/ca-certificates and runs update-ca-certificates, making the trust available to .NET, OpenSSL, curl, and other system components. Multiple certificates can be provided by pointing EXTRA_CA_CERT at a directory containing .crt or .pem files.

In highly restricted environments where you cannot modify the system trust store, you can instead rely on the standard SSL_CERT_FILE or SSL_CERT_DIR environment variables:

docker run \
  -v /path/to/company-ca-bundle.pem:/certs/custom.pem:ro \
  -e SSL_CERT_FILE=/certs/custom.pem \
  elsaworkflows/elsa-server-and-studio-v3:latest

ℹ️ Installing the CA bundle adds roughly 300KB to the Debian-based images. No package managers run at container startup; all trust updates happen immutably at build time or via the mounted certificates shown above.

Table of Contents

Documentation

Elsa Documentation Website.

Known Issues and Limitations

Elsa is continually evolving, and while it offers powerful capabilities, there are some known limitations and ongoing work:

  • Documentation is still a work in progress.
  • Input/Output is not yet implemented in the Workflow Instance Viewer.
  • Starting workflows from the designer is currently supported only for workflows that do not require input and do not start with a trigger; this is planned for a future release.
  • The designer currently only supports Flowchart activities. Support for Sequence and StateMachine activities is planned for a future release.
  • UI input validation is not yet implemented.

Features

Elsa offers a wide range of features for building and executing workflows, including:

  • Execution of workflows in any .NET application with support for .NET 6 and beyond.
  • Support for both short-running and long-running workflows.
  • A programming model loosely inspired by Windows Workflow Foundation.
  • A web-based drag & drop designer with support for custom activities.
  • Native support for activity composition, including activities like Sequence, Flowchart, and ForEach.
  • Parallel execution of activities.
  • Built-in activities for common scenarios, such as sending emails, making HTTP calls, scheduling tasks, sending and receiving messages, and more.
  • Workflow versioning and migration via API.
  • Easy integration with external applications via HTTP, message queues, and more.
  • Actor model for increased workflow throughput.
  • Dynamic expressions with support for C#, JavaScript, Python, and Liquid.
  • Persistence agnostic, with support for Entity Framework Core, MongoDB, and Dapper out of the box.
  • Elsa Studio: a modular Blazor dashboard app for managing and designing workflows.

Roadmap

See ROADMAP.md for the current roadmap and #3232 for historical roadmap discussion.

Use Cases

Elsa can be used in a variety of scenarios, including:

  • Long-running workflows such as order fulfillment and product approval.
  • Short-running workflows such as sending emails and generating PDFs.
  • Scheduled workflows such as sending daily reports.
  • Event-driven workflows such as sending welcome emails when a user signs up.

Coding Workflows

Elsa allows you to define workflows in code using C#. The following example shows how to receive HTTP requests and send an email in response:

public class SendEmailWorkflow : WorkflowBase
{
    protected override void Build(IWorkflowBuilder builder)
    {
        builder.Root = new Sequence
        {
            Activities =
            {
                new HttpEndpoint
                {
                    Path = new("/send-email"),
                    SupportedMethods = new(new[] { HttpMethods.Post }),
                    CanStartWorkflow = true
                },
                new SendEmail
                {
                    From = new("alic@acme.com"),
                    To = new(new[]{ "bob@acme.com" }),
                    Subject = new("Your workflow has been triggered!"),
                    Body = new("Hello!")
                }
            }
        };
    }
}

Designing Workflows

Elsa allows you to define workflows using a visual designer. The following example shows how to receive HTTP requests and send an email in response:

Elsa ships with a powerful visual designer

Contributing

We welcome contributions from the community and are pleased that you are interested in helping to improve the Elsa Workflow project! Here are the steps to contribute to our project:

1. Fork and Clone the Repo

To get started, you'll need to fork the repository to your own GitHub account. You can do this by navigating to the Elsa Workflow GitHub repository and clicking the "Fork" button in the top-right corner of the page. Once you have forked the repo, you can clone it to your local machine using the following command:

git clone https://github.com/YOUR_USERNAME/elsa-core.git

Replace YOUR_USERNAME with your GitHub username. For more information on forking a repo, check out the GitHub documentation here.

Incorporating the details about the "apps" folder and its projects into the second point about opening the Elsa.sln using your favorite IDE, we can expand the instructions to guide developers on where to start and what projects they might want to explore first. Here's an updated version of that section with the additional information:

2. Open Elsa.sln Using Your Favorite IDE

After cloning the repository, navigate to the cloned directory and open the Elsa.sln solution file with your preferred IDE that supports .NET development, such as Visual Studio, JetBrains Rider, or Visual Studio Code with the appropriate extensions.

Within the solution, you will find an "apps" folder containing three projects designed to help you get started and explore the capabilities of Elsa Workflow:

  • Elsa.Server.Web: This project is a reference ASP.NET Core application that acts as a workflow server. It's a great starting point if you want to understand how Elsa functions as a server-side workflow engine.

  • Elsa.ServerAndStudio.Web: This project serves a dual purpose. Like Elsa.Server.Web, it acts as a workflow server. Additionally, it hosts the Elsa Studio Blazor WebAssembly app. This is the perfect project to run if you want to see the full capabilities of Elsa, including both the server aspects and the client-side studio experience in one application.

  • Elsa.Studio.Web: This project is a reference Blazor WebAssembly application that solely hosts the Elsa Studio Blazor WebAssembly app. It requires a running Elsa server application to connect to. Use this project if you're interested in focusing on the Elsa Studio UI and its interactions with an Elsa workflow server.

3. Submit a PR with Your Changes

Once you have made your changes, commit them and push them back to your fork. Then, navigate to the original Elsa Workflow repository and create a new Pull Request. Ensure your PR description clearly describes the changes and any relevant information that will help the reviewers understand your contributions. For a detailed guide on creating a pull request, visit Creating a pull request from a fork.

4. Open an Issue First

Before you start working on your changes or submit a pull request, please open an issue to discuss what you would like to do. This step is crucial as it ensures you don't spend time working on something that might not align with the project's goals or might already be under development by someone else. You can open an issue here.

This approach helps us streamline contributions and ensures that your efforts are aligned with the project's needs and priorities. We look forward to your contributions and are here to support you throughout the process. Thank you for contributing to the Elsa Workflow project!

Support

There are various ways to get support for Elsa Workflows, ranging from community-driven channels to enterprise-level services.

Community Support

Elsa has an active and helpful community where you can find support through multiple channels:

  • GitHub Issues for bug reports and feature requests.
  • GitHub Discussions for open-ended conversations, questions, and community-driven support.
  • Discord for real-time support and interaction with the Elsa community.
  • StackOverflow for searching or asking technical questions.

Professional Support

For organizations requiring professional support and long-term commitment, check out Elsa+, a growing ecosystem of premium services, tooling, and extensions around Elsa Workflows.