Introduction to Building Workflow Driven .NET Applications with Elsa 2

Introduction to the multi-part series on building workflow driven applications with Elsa and .NET

Sipke Schoorstra
7 min readJul 31, 2021

In my previous article about building workflow driven .NET Core applications with Elsa, we looked at integrating Elsa 1 into your .NET Core application.

A lot has changed between V1 and V2 and the best way to get a feel for the differences is to see what it takes to create an actual application with this new version.

We’ll walk through the creation of a new ASP.NET Core project and install Elsa packages and build up the demo project step by step.

Here’s what we’ll be doing:

  • Setup Elsa 2 as an in-process workflow engine (internal to the ASP.NET Core application)
  • Design workflows with Elsa Studio and export them to JSON files
  • Use the workflow JSON files to execute workflows
  • Programmatically execute workflows
  • Write a custom activity that represents a long-running job
  • Add a custom JavaScript function with intellisense support
  • Send files as email attachments
  • Implement long-running workflows

Because there’s a substantial amount of code editing involved, we’ll be doing all this in separate parts:

The complete solution can be found here.

The Sample Project

The sample project is called Document Management and offers the following features:

  • Ability to upload a document and specify the type of document.
  • The document metadata is stored in a SQLite database while the physical file is stored on disk.
  • The application executes any & all workflows associated with the selected document type.

The uploaded document gets processed by any workflows in the system configured to handle this type of document. A workflow might compress the file and send it as an attachment, request a review from a person, compute a hash and store it on a blockchain, and so on and so forth. The possibilities are endless, and workflows make it easy to quickly setup new processes and change existing ones.

The following screen recording provides a quick glance of the final result.

For this series we will be implementing the following document types and corresponding workflows:

  • Change Request
  • Leave Request
  • Identity Verification

The Workflows

The following sections describe the functionality of each workflow. Although the workflows merely serve as an example, the point is to demonstrate key-principles and how one might implement real-world scenarios by creating custom activities that connect to services, databases and APIs, perform long-running jobs, extend JavaScript with custom functions, pass along information through activities and implement long-running processes that involve the user.

Let’s go over each one of them from a high-level perspective.

Change Request Workflow

This workflow handles documents of type Change Request and will simply zip the received document file and send it as an attachment, after which the document will be marked as archived.

The workflow looks like this:

This workflow does not contain any control flow logic — it’s a simple sequence of steps. We’ll discuss each activity separately.

Leave Request Workflow

This workflow handles documents of type Leave Request. Leave requests can be submitted to the application of you want to e.g. go on a holiday. The workflow will send an email to HR and await an Approve or Reject signal before continuing. At the end, the document will be archived.

Identity Verification Workflow

This workflow handles documents of type Identity Verification, and is similar to the Leave Request workflow. The main difference is that this one will include a custom activity that performs a background task using Hangfire, during which the workflow will be suspended. Once the background task completes, the workflow will be loaded back into memory and resume execution.

The Activities

Let’s go over each activity used in the various workflows.

Get Document

A custom activity that has a single input property called DocumentId.

The document ID is used to retrieve the document record from the database. This document contains the file name of the uploaded file and is used to load the physical file into memory as a Stream.

Both the document and the file stream are then made available as the output of the activity.

Subsequent activities can then use this output. For example, the Send Email activity can be configured to send the file as an attachment.

Archive Document

A custom activity that has a single input property called Document of type Document. Notice that this is different when compared to the Get Document activity, which only needs a document ID. Therefore, these activities work together: first we load a document, and then we have activities that do something with this document.

We could have designed this activity to also accept just a document ID and have the activity load the document, but the reason we’re doing it differently here is to demonstrate that we can have activities provide any type of output such as complex objects and process them in a variety of ways.

Zip File

A custom activity that compresses an input stream into an output stream.

Update Blockchain

A custom activity that pretends it will store a file signature on some blockchain. The real purpose of this activity is to demonstrate how one might implement an activity that starts a background task using Hangfire while the workflow is suspended and then resuming the workflow once the job completed.

Send Email

An activity provided by the Elsa.Activities.Email package that can send email messages.


An activity provided by Elsa.Core that splits workflow execution in multiple branches. Each of these branches will execute one after another. A fork is typically used when you want to do work in parallel and/or if you want to wait for (one of) multiple triggers simultaneously. A common example is that of a race condition between waiting for user input within a specified amount of time and waiting for a timeout event.


An activity that joins workflow execution back into a single branch. It is used in combination with the Fork activity and can have one of two modes:

  • Wait All
  • Wait Any

The Wait All mode will cause the Join activity to wait for all inbound branches to enter the Join activity. When there are no blocking activities in any of the branches, the Join activity will simply continue execution. But if there are any number of branches blocked, the activity will continue only once each blocking activity is resumed. This mode is useful for scenarios where you for example need to wait for all parties to sign a document before continuing.

This is in contrast to the Wait Any mode, which will cause the Join activity to continue as soon as any one inbound branch enters. This mode is useful for scenarios where there’s a user action to be taken within a given amount of time, a race condition modeled using for example the Timer activity and the Signal Received activity.

The Leave Request and Identity Verification both demonstrate the use of the Fork and Join activity using the Wait Any mode.

Any blocking activities between the Fork and Join activities will be cleared. This means that as soon as the Join activity continues execution, any trigger like timers and signals will no longer be handled.

Signal Received

An activity acting as a workflow trigger. When encountered, the activity will suspend the workflow until a certain signal is received. When the correct signal is received, the workflow is resumed.

A common use case is waiting for some user input to be received, as we will see.

Write Line

A simple activity provided by the Elsa.Activities.Console package. This activity takes a single string input and writes it to standard out (typically the console).

This activity is often used for testing out certain pathways in a workflow or as a (temporary) stand-in while the “real” activity is being developed.


In the next part, we will scaffold a new .NET solution with basic workflow capabilities. Subsequent parts will tackle implementing custom activities, extensibility, the demo workflows and all other nitty-gritty details we need.