# Elsa Workflows 3

Introducing Elsa Workflows 3

Elsa Workflows is a set of open-source .NET libraries designed to enhance .NET applications with workflow capabilities. Think of it as lego blocks for creating workflow engines in .NET.

Workflows in Elsa can be defined in different ways:

* Programmatically by writing .NET code.
* Visually using the built-in designer, non-developers or those who prefer a visual approach can create and modify workflows with ease.
* Declaratively using JSON.

## Defining Workflows

Define workflows programmatically using .NET code:

```csharp
// Define workflows directly from code.
var workflow = new Sequence
{
    Activities =
    {
        new WriteLine("Hello World!"),
        new Delay(TimeSpan.FromSeconds(1)),
        new WriteLine("It is nice to meet you!")
    }
};
```

And/or, define workflows visually using [Elsa Studio](/application-types/elsa-studio):

<div data-full-width="false"><figure><img src="/files/fZt5sjxNU1tF2pLnldTm" alt=""><figcaption><p><kbd>Design workflows visually</kbd></p></figcaption></figure></div>

And/or, define workflows declaratively using JSON:

```json
{
  "id": "1",
  "definitionId": "1",
  "name": "Hello World",
  "version": 1,
  "root": {
    "id": "Flowchart1",
    "type": "Elsa.Flowchart",
    "activities": [
      {
        "id": "WriteLine1",
        "type": "Elsa.WriteLine",
        "text": {
          "typeName": "String",
          "expression": {
            "type": "Literal",
            "value": "Hello World!"
          }
        }
      }
    ]
  }
}
```

## Use Cases

1. **Integrate Workflow Execution in .NET Applications**: Add workflow execution capabilities to existing applications.
2. **Standalone Workflow Server**: Deploy Elsa as an independent workflow server to manage various business processes across your organisation.
3. **Prebuilt Docker Containers**: Instead of building a custom workflow server, there is the option of hosting a prebuilt Docker image that acts as a standalone workflow server that exposes REST APIs to interact with the engine.

## Key Features

1. **Long & Short Running Workflows**: Workflows can both be short-lived completing in milliseconds, as well as long-lived, potentially spanning hours, days, weeks, months and even years.
2. **Activity Library**: Elsa offers a rich set of out-of-the-box activities, providing essential building blocks to construct flexible and effective workflows tailored to your business needs.
3. **Triggers**: Workflows can be initiated automatically based on specific events or conditions, enabling seamless automation and integration with existing processes.
4. **Dynamic Expressions**: Elsa allows the use of C#, JavaScript, or Liquid expressions for dynamic value evaluation during runtime, enhancing workflow logic. You can customize it as Elsa is highly modular and extensible.
5. **Extensibility**: Elsa is built to be extensible, making it easy to add custom activities and connect with other systems.
6. **Reusable Web-Based Designer**: With the web-based drag & drop designer hosted in Elsa Studio, users can visually create workflows, leveraging a modular and extensible framework.
7. **Scalable Performance**: Elsa is designed for high performance, working across multiple nodes in a cluster for horizontal scaling.

## Known 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.
* 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.


# Concepts

This section provides a comprehensive overview of fundamental principles and key elements that form the foundation of Elsa.

## Workflow

A workflow is a sequence of steps called **activities** that represents a process. Workflows can be created visually or programmatically. In Elsa, a workflow is represented by an instance of the `Workflow` class. The Workflow class has a `Root` property of type `IActivity`, which is scheduled for execution when the workflow starts.

## Workflow Instance

A workflow instance represents a database-persisted instance of a workflow in execution, encapsulated by the `WorkflowInstance` class.

[Read more](/getting-started/concepts/workflow-context)

## Activity

An activity is a unit of work executed by the workflow engine. In Elsa, these are classes implementing the `IActivity` interface and can be linked or composed together to form a workflow.

## Bookmark

A bookmark signifies a pause point in a workflow, enabling the workflow to be resumed later. It is typically created by blocking activities such as the `Event` or `Delay` activity.

## Trigger

A trigger is an activity with its `Kind` metadata set to `Trigger` and is able to start new workflow instances of the containing workflow. For example, the `HttpEndpoint` activity is a trigger that enables the containing workflow to be executed when a given URL is requested.

## Blocking Activity

Blocking activities are those which do not complete execution immediately upon initiation. They often create bookmarks, halting the workflow's progress until resumed. This halting nature coins the term "blocking."

## Burst of Execution

This term describes the period during which the workflow runner actively executes activities. A workflow executing continuously from start to finish occurs in a single burst, whereas a workflow interrupted by a blocking activity results in multiple bursts, resuming on subsequent triggers.

## Correlation ID

A Correlation ID is a flexible identifier linking related workflows and external entities. It aids in tracing workflows in distributed, asynchronous, or hierarchical systems. Assigning a Correlation ID allows tracking of related workflows and ties them to specific business objects like documents, customers, or orders.

[Read more](/getting-started/concepts/correlation-id)

## Outcome

Activities in a flowchart are connected that defines the logic of the workflow. Each activity can have one ore more *potential* results, which are referred to as *outcomes*. These outcomes are visually displayed as "ports" on the activity. For example, the `Decision` activity has two potential outcomes: `True` and `False`. When using the designer, the user can connect a subsequent activity to these outcomes. This powerful mechanism simplifies workflows by eliminating the need for separate decision activities to evaluate an activity's result.

[Read more](/getting-started/concepts/outcomes)

## Input

In Elsa, input can refer to two things:

* Input to an activity.
* Input to a workflow.

### Activity Input

Most activities have at least one input, represented as public properties. For instance, the `WriteLine` activity has a `Text` property used to display a string in the console window.

### Workflow Input

Workflows can receive input from the application. For example, a workflow processing an order can get the Order ID through an input like `OrderId`.

## Output

In workflows, activities can produce *output* data for later steps. Outputs pass info like numbers or text to the next activity. Activities can create results and outputs. You can generate outputs, like booleans, for decision-making, but it's easier to use outcomes. Use outputs for data, like database results, when no decisions are needed.

## Variable

Variables can be set at the workflow level to store data. Use dynamic expressions to set or retrieve these variables. Activity outputs can update a variable automatically, allowing them to be used by the next activities. This makes it easy to transfer and store data for activities.

{% hint style="info" %}
**In Elsa Studio**: Learn how to reference variables in expressions using JavaScript and C# in the [Expressions guide](/guides/studio/expressions).
{% endhint %}

[Read more](/getting-started/concepts/workflow-context)

## Incident

An incident is an error event that occurred in the workflow. For example, if an activity faults, an incident is recorded as part of the workflow execution.

## Alteration

An alteration represents a change that can be applied to a given [workflow instance](#workflow-instance).

Using alterations, you can modify a workflow instance's state, schedule activities, and more.

## Workflow Context

Workflow execution in Elsa spans workflow-level state, activity-level state, scoped variables, bookmarks, incidents, and execution logs.

[Read more](/getting-started/concepts/workflow-context)

If you want the runtime view that connects triggers, bookmarks, stimuli, dispatch, and persistence, read [Execution Model](/guides/architecture/execution-model).


# Workflow Context

Understand what Elsa 3.8 stores in workflow state, activity state, variables, inputs, outputs, bookmarks, incidents, and execution logs.

Elsa workflows execute with more than one kind of state in play. Some state belongs to the workflow instance as a whole, some belongs to the currently active activity stack, and some is only available while expressions are being evaluated.

This page explains how that fits together in Elsa `release/3.8.0`, and where each piece shows up in code, APIs, and Elsa Studio.

## The three layers

### Workflow definition

The workflow definition is the published design: activities, variables, inputs, outputs, outcomes, options, and metadata.

In code-first workflows, these are declared on `IWorkflowBuilder`:

```csharp
protected override void Build(IWorkflowBuilder builder)
{
    var customerId = builder.WithInput<string>("CustomerId");
    builder.WithOutput<string>("Result");
    var status = new Variable<string>("Status", "Pending");

    builder.WithVariable(status);
    builder.Root = new Sequence
    {
        Activities =
        {
            new WriteLine(context => $"Customer: {context.GetInput<string>(customerId)}")
        }
    };
}
```

This matches the `Inputs`, `Outputs`, and `Variables` collections on `WorkflowBuilder`.

### Workflow instance

A workflow instance is the persisted runtime state for one execution. In Elsa 3.8, `WorkflowState` stores:

* definition identity and version
* workflow `Input`
* workflow `Output`
* workflow `Properties`
* `Bookmarks`
* `Incidents`
* active `ActivityExecutionContexts`
* scheduled work items and completion callbacks
* timestamps and execution status

That is the state you inspect in the instance viewer, structured logs, and workflow-instance APIs.

### Activity execution context

Each active activity executes inside an `ActivityExecutionContext`. This is where Elsa keeps activity-local runtime data such as:

* evaluated activity input state in `ActivityState`
* activity-specific `Properties`
* lightweight `Metadata`
* `DynamicVariables`
* activity `JournalData`
* bookmarks created by the activity

When a workflow is suspended, Elsa persists the active activity execution contexts as a flattened call stack inside workflow state and reconstructs them when resuming.

## What lives where

| Concern                  | Where it lives                                                     | Notes                                                                                     |
| ------------------------ | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| Workflow inputs          | `WorkflowExecutionContext.Input` / `WorkflowState.Input`           | Persisted only for inputs configured with a workflow or workflow-instance storage driver. |
| Workflow outputs         | `WorkflowExecutionContext.Output` / `WorkflowState.Output`         | Written explicitly, typically with `SetOutput`.                                           |
| Workflow properties      | `WorkflowExecutionContext.Properties` / `WorkflowState.Properties` | Global property bag for application or activity data.                                     |
| Variables                | Memory blocks referenced by `Variable` objects                     | Scoped through the expression context chain.                                              |
| Activity input snapshots | `ActivityExecutionContext.ActivityState`                           | Persisted for historical/runtime inspection, subject to activity-state filtering.         |
| Bookmarks                | `WorkflowExecutionContext.Bookmarks` / `WorkflowState.Bookmarks`   | Pause and resume points for blocking work.                                                |
| Incidents                | `WorkflowExecutionContext.Incidents` / `WorkflowState.Incidents`   | Recorded failures and diagnostic information.                                             |
| Execution log            | `WorkflowExecutionContext.ExecutionLog` plus persisted log records | Used for the journal and structured log views.                                            |

## Variables and scoping

Variables in Elsa are memory block references. A variable declared as `new Variable<string>("Foo", "Bar")` gets a deterministic ID derived from its name and is resolved through the current expression-context chain.

Variables can be declared at more than one level:

* workflow level
* container level, such as `Sequence`
* dynamically at runtime

Scope matters. Elsa resolves variables from the nearest matching scope first. The integration tests on `release/3.8.0` verify that when two scopes define `Foo`, `SetVariable` updates the nearest one.

```csharp
var workflowLevelVariable = new Variable<string>("Foo", "Workflow Value");
var sequenceLevelVariable = new Variable<string>("Foo", "Initial Value");

workflow.Root = new Sequence
{
    Variables = { workflowLevelVariable },
    Activities =
    {
        new Sequence
        {
            Variables = { sequenceLevelVariable },
            Activities =
            {
                new SetVariable
                {
                    Variable = sequenceLevelVariable,
                    Value = new("Sequence Value")
                },
                new WriteLine(context => context.GetVariable<string>("Foo"))
            }
        }
    }
};
```

Use variables when the data belongs to workflow state that several activities need to read or update over time.

Use workflow inputs when the caller supplies the value at start or resume time.

Use workflow outputs when the workflow needs to return named results to the caller or parent workflow.

## Inputs

Workflow inputs are declared on the workflow definition and stored in the workflow input bag at runtime.

In code, you usually read them in one of two ways:

```csharp
var message = builder.WithInput<string>("Message");

builder.Root = new WriteLine(context => context.GetInput<string>(message));
```

```csharp
builder.Root = new WriteLine(context => context.GetWorkflowInput<string>("Message"));
```

There is one important nuance in Elsa 3.8: `ExpressionExecutionContext.GetInput(name)` first checks for a variable with that name when the current activity executes inside a composite activity. If such a variable exists, that variable value wins over the workflow input.

For Studio users, workflow inputs are defined on the workflow's **Inputs** tab and can then be referenced from activity editors and expressions.

## Outputs

Workflow outputs are named values declared on the workflow definition. Elsa persists them in `WorkflowExecutionContext.Output` and `WorkflowState.Output`.

Code-first workflows typically declare outputs with `WithOutput<T>()` and set them with the `SetOutput` activity:

```csharp
var valueInput = builder.WithInput<double>("Value");
var output = builder.WithOutput<double>("Output");

builder.Root = new Sequence
{
    Activities =
    {
        new SetOutput
        {
            OutputName = new(output.Name),
            OutputValue = new(context => context.GetInput<double>(valueInput) * 2)
        }
    }
};
```

`SetOutput` writes to the nearest ancestor output when running inside a composite or workflow-as-activity scenario, and also updates the root workflow output bag when needed.

For Studio users, workflow outputs are defined on the **Outputs** tab and are shown in the workflow instance viewer under **Input/output**.

## Activity state versus variable state

This is a common source of confusion:

* Variables are live workflow data meant to be read and updated by activities and expressions.
* `ActivityState` is Elsa's persisted snapshot of evaluated activity input values for an activity execution context.

`ActivityState` is useful for diagnostics, replay context, and historical inspection. It is not the main API you should use to exchange data between activities.

Elsa can filter activity state before persistence. For example, the sample server registers an activity-state filter that strips HTTP authentication header values from persisted HTTP request state.

## Bookmarks and suspension

Bookmarks represent places where a workflow can pause and later resume. Blocking activities create them. They are stored on workflow state, not on a separate variable system.

When a workflow blocks:

1. active activity execution contexts are persisted
2. bookmarks are stored on `WorkflowState.Bookmarks`
3. the workflow resumes later by selecting the matching bookmark

This is why long-running workflows survive process restarts as long as runtime persistence is configured correctly.

## Incidents

Incidents are workflow-level records of execution failures. They are stored in `WorkflowState.Incidents` and surfaced in Elsa Studio's workflow instance viewer.

Use incidents when you want to understand:

* which activity failed
* the exception message and stack trace
* whether a workflow needs retry, cancellation, or manual intervention

## Execution logs and journal data

Elsa records execution-log entries such as `Started`, `Resumed`, `Suspended`, `Completed`, and `Faulted` during workflow and activity execution.

Activity execution contexts also expose `JournalData`, which is appended to execution-log records. Elsa uses that to record details such as selected outcomes and serialized output values.

Use the journal when you need an execution timeline.

Use variables, inputs, outputs, and properties when you need the current state of the workflow.

## Elsa Studio mapping

For workflow authors using Studio:

* **Variables**, **Inputs**, and **Outputs** are workflow-definition tabs.
* The workflow instance viewer shows **Variables**, **Input/output**, and **Incidents** for a running or completed instance.
* The alteration designer can also load and modify workflow instance variables.
* Expression editors can read workflow inputs and variables; see [Expressions in Elsa Studio](/guides/studio/expressions).

## Inspecting and updating instance variables

If you need to inspect or correct variable values on a live workflow instance, use the variable-management API or `IWorkflowInstanceVariableManager`.

That operational workflow is covered in [Workflow Instance Variables](/operate/workflow-instance-variables).

## When to use what

Use this rule of thumb:

* Use **inputs** for caller-supplied values.
* Use **variables** for mutable workflow state shared across steps.
* Use **outputs** for named results.
* Use **properties** for application-owned workflow metadata.
* Use **bookmarks** to understand why a workflow is waiting.
* Use **incidents** and the **journal** to diagnose faults and execution history.


# Outcomes

### Understanding Outcomes

Activities are connected to form a flowchart that defines the logic of the workflow. Each activity can have multiple potential results, which are referred to as *outcomes*. These outcomes serve as "ports" that dictate the next step in the workflow based on the result of the activity. This powerful mechanism simplifies workflows by eliminating the need for separate decision activities to evaluate an activity's result.

#### What is an Outcome?

An outcome is a possible result of an activity's execution. Once an activity completes, the workflow can continue down different paths depending on which outcome was produced. For instance, an approval activity might have two outcomes: "Approved" and "Rejected." By connecting these outcomes directly to other activities in the workflow, users can control the flow without the need for intermediate decision activities.

#### How to Use an Outcome?

Outcomes are exposed as "ports" or connectors from one activity to the next. When defining a workflow, you can simply connect an activity's outcome to the next relevant activity in the flowchart. This provides a visual and intuitive way of representing decision logic.

For example:

* A "Send Email" activity might have two outcomes: "Success" and "Failure."
* Depending on the outcome, you could connect "Success" to a "Log Activity" and "Failure" to a "Retry" activity.

#### Outcomes in Nested Workflows

Workflows can be used as activities within other workflows, and they too can define their own outcomes. When creating a workflow that you plan to use as a nested activity inside another workflow, you can define its outcomes to represent different execution paths. This allows parent workflows to continue based on the outcome of the nested workflow, offering modularity and flexibility in complex scenarios.

#### Difference Between an Outcome and an Output

While both *outcomes* and *outputs* represent information produced by an activity, they serve different purposes:

* **Outputs**: These are data or results generated by an activity that can be used later in the workflow. Outputs allow activities to pass values (such as a number, string, or object) to subsequent activities for processing or storage.
* **Outcomes**: These define the flow of the workflow itself, determining which path to follow based on how the activity completes. Instead of providing data, outcomes shape the control flow by specifying which activity should run next.

For instance, a "Form Submission" activity might have an *output* containing form data (e.g., the values entered by the user), while its *outcome* might determine whether to send a confirmation email or display an error message depending on whether the form validation was successful or not.

#### Benefits of Using Outcomes

* **Simplification of Flow Logic**: By using outcomes, workflows become more streamlined since there is no need for decision nodes to handle activity results.
* **Visual Clarity**: The flowchart clearly shows which path follows from each possible outcome, making it easier to understand at a glance.
* **Modularity**: Workflows and activities can be reused more easily when outcomes are well-defined, enabling a flexible and scalable workflow design.

In summary, outcomes provide a simple and effective way to direct the flow of your workflows based on how activities conclude. By distinguishing between outcomes and outputs, users can manage both the logical flow and the data flow in their workflows with clarity and ease.


# Correlation ID

### Overview

A **Correlation ID** is a flexible identifier used to associate related workflow instances with each other and with external domain entities. This feature is particularly useful in scenarios where workflows are distributed, triggered asynchronously, or involve parent-child relationships. By assigning a Correlation ID to workflow instances, users can trace and analyze the journey of related workflows as they execute across systems or tie them to specific business objects like documents, customers, or orders.

### How is Correlation ID Used?

A **Correlation ID** is typically used to link workflow instances that are logically connected but run independently. The Correlation ID can also link workflows to specific domain entities. Here are some common use cases:

* **Parent-Child Workflow Relationship**: When a parent workflow dispatches child workflows (e.g., via message queues like RabbitMQ), each child workflow can share the same Correlation ID as the parent. This allows users to trace the entire execution chain across multiple workflows.
* **Correlating Workflows with Domain Entities**: In business processes, workflows often operate on domain entities such as **Documents**, **Customers**, **Orders**, or **Transactions**. A workflow can use a Correlation ID based on these entities' unique identifiers to track workflows related to a specific domain object.
* **Multi-Step Processes**: In long-running processes where different workflows represent different stages (e.g., order processing, shipping, billing), the same Correlation ID can be used to tie these stages together, enabling users to monitor and manage the entire process as one cohesive operation.
* **Distributed Systems**: In distributed systems where workflows may be triggered by events or messages across multiple services, the Correlation ID ensures that related workflows are easily identifiable, even though they run in different contexts or environments.

### When is Correlation ID Assigned?

The Correlation ID can be assigned in two ways:

1. **Manual Assignment**: When creating or dispatching a workflow instance, the Correlation ID can be explicitly set via API calls or workflow triggers. This is useful when the Correlation ID is already known, such as when the workflow is part of a larger business process or is tied to a specific domain entity like a customer or order.
2. **Correlate Activity**: You can use the **Correlate Activity** within the workflow to dynamically assign or update the Correlation ID during workflow execution. This is especially useful when the Correlation ID is only known after the workflow has started. For example, in a workflow processing an order, you can use a JavaScript expression that retrieves the **Order ID** (or any other domain-specific identifier) and assigns it as the Correlation ID. This ensures that the workflow is correlated with the appropriate entity, even if the identifier is determined at runtime.

   ```javascript
   // Example JavaScript expression to set the Correlation ID
   getOrder().Id
   ```

   In this scenario, the **Correlate Activity** will associate the current workflow instance with the provided Correlation ID, allowing it\
   to be linked to other workflows or entities, such as customers or documents. This method is powerful for scenarios where the Correlation ID depends on dynamic data that is only available during the execution of the workflow.

### Correlation with Domain Entities

One of the most powerful features of the Correlation ID is its ability to link workflows to domain entities. Here are some examples:

#### Documents

A document processing system may involve multiple workflows to handle the lifecycle of a document (e.g., reviewing, approving, and archiving). Each of these workflows can be assigned the **Document ID** as the Correlation ID, ensuring that all workflows related to the same document can be tracked together.

#### Customers

In customer-facing systems, workflows might handle different aspects of a customer lifecycle, such as registration, onboarding, and support. By using the **Customer ID** as the Correlation ID, you can group workflows that relate to the same customer, providing a unified view of all processes associated with them.

#### Orders

For e-commerce or order management systems, workflows often manage different stages of order fulfilment (e.g., processing, shipping, invoicing). Assigning the **Order ID** as the Correlation ID allows you to track all workflows involved in completing an order, ensuring traceability from the moment the order is placed to its delivery.

### Restrictions on Correlation ID

There are minimal restrictions on what can be used as a Correlation ID, making the system very flexible. However, some best practices are recommended:

* **String Format**: The Correlation ID is a string. It can be any alphanumeric value, UUID, or any other string-based identifier that suits the application's needs.
* **Uniqueness Across Instances**: While Elsa does not enforce uniqueness, the Correlation ID should ideally be unique across logically distinct workflow groups to avoid confusion during tracking and monitoring. For example, a **Document ID** or **Customer ID** should be used consistently to avoid overlapping identifiers.
* **Length and Characters**: Although Elsa does not impose strict length restrictions, it is recommended to keep the Correlation ID concise and easily readable. Avoid special characters that might interfere with logging systems or external tools used for monitoring.

### Monitoring and Observability

In many applications, workflows emit telemetry signals to monitoring tools (such as OpenTelemetry) that help track their execution. The **Correlation ID** plays a central role in this by ensuring that all related workflows are grouped together in the telemetry data. This makes it easy to:

* **Trace execution paths**: Visualize how data and actions flow between related workflows and domain entities.
* **Identify bottlenecks**: Quickly find delays or errors in a sequence of related workflows tied to a specific domain entity.
* **Improve debugging**: Simplify troubleshooting by focusing on the entire set of related workflows using the Correlation ID.

### Conclusion

The Correlation ID is a powerful tool for associating and tracking related workflow instances, as well as linking workflows to domain entities like **Documents**, **Customers**, or **Orders**. By leveraging Correlation IDs, developers can implement workflows that are traceable, observable, and better suited for complex, distributed, or asynchronous systems. The flexibility in assigning Correlation IDs allows them to be customized to the needs of any application or business process, ensuring easy integration with monitoring and observability solutions.


# Architecture Overview

Comprehensive architecture guide covering Elsa's components, execution model, data flow, and deployment patterns for architects and integrators.

This guide provides a comprehensive overview of Elsa Workflows' architecture, covering the major components, execution model, data flow, scalability considerations, and deployment topologies.

## High-Level Architecture

Elsa Workflows is built on a modular, extensible architecture designed for flexibility and scalability. The system consists of several key layers that work together to enable workflow definition, execution, and management.

```
┌─────────────────────────────────────────────────────────────┐
│                      Presentation Layer                      │
│  ┌────────────────┐  ┌────────────────┐  ┌───────────────┐ │
│  │  Elsa Studio   │  │  REST APIs     │  │  SignalR Hub  │ │
│  │  (Blazor WASM) │  │                │  │               │ │
│  └────────────────┘  └────────────────┘  └───────────────┘ │
└─────────────────────────────────────────────────────────────┘
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                      Application Layer                       │
│  ┌──────────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │  Workflow        │  │  Activity    │  │  Trigger     │  │
│  │  Management      │  │  Registry    │  │  System      │  │
│  └──────────────────┘  └──────────────┘  └──────────────┘  │
└─────────────────────────────────────────────────────────────┘
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                    Workflow Runtime Layer                    │
│  ┌──────────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │  Workflow        │  │  Bookmark    │  │  Workflow    │  │
│  │  Execution       │  │  Manager     │  │  Dispatcher  │  │
│  │  Engine          │  │              │  │              │  │
│  └──────────────────┘  └──────────────┘  └──────────────┘  │
└─────────────────────────────────────────────────────────────┘
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                      Persistence Layer                       │
│  ┌──────────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │  Workflow        │  │  Activity    │  │  Execution   │  │
│  │  Definitions     │  │  Execution   │  │  Logs        │  │
│  │  & Instances     │  │  Records     │  │              │  │
│  └──────────────────┘  └──────────────┘  └──────────────┘  │
│                    (EF Core / MongoDB)                       │
└─────────────────────────────────────────────────────────────┘
```

## Major Components

### 1. Elsa Server

The **Elsa Server** is an ASP.NET Core application that hosts the workflow execution engine and exposes REST APIs for workflow management and execution.

**Key Responsibilities:**

* **Workflow Execution**: Runs workflow instances using the workflow runtime
* **API Endpoints**: Exposes REST APIs for managing workflows, activities, and workflow instances
* **Trigger Processing**: Listens for and processes workflow triggers (HTTP, Timer, Events, etc.)
* **Background Services**: Manages scheduled tasks, long-running workflows, and delayed activities
* **Authentication & Authorization**: Controls access to workflow management and execution

**Key Packages:**

* `Elsa.Workflows.Core` - Core workflow engine
* `Elsa.Workflows.Runtime` - Workflow runtime and execution services
* `Elsa.Workflows.Management` - Workflow definition and instance management
* `Elsa.Workflows.Api` - REST API endpoints

**Configuration Example:**

```csharp
builder.Services.AddElsa(elsa =>
{
    // Configure workflow management
    elsa.UseWorkflowManagement(management => 
        management.UseEntityFrameworkCore());
    
    // Configure workflow runtime
    elsa.UseWorkflowRuntime(runtime => 
        runtime.UseEntityFrameworkCore());
    
    // Enable API endpoints
    elsa.UseWorkflowsApi();
    
    // Enable real-time updates
    elsa.UseRealTimeWorkflows();
});
```

### 2. Elsa Studio

**Elsa Studio** is a Blazor application built as a Razor Class Library (RCL) that provides a visual designer for creating and managing workflows through a browser-based interface. It can be hosted using any Blazor hosting model, including Blazor WebAssembly, Blazor Server, or embedded in other Blazor applications.

**Key Features:**

* **Visual Workflow Designer**: Drag-and-drop interface for building workflows
* **Activity Configuration**: Rich UI for configuring activity properties and expressions
* **Workflow Execution Monitoring**: Real-time view of running workflows
* **Instance Management**: Browse and manage workflow instances
* **Execution History**: View detailed execution logs and activity traces

**Architecture:**

* Blazor WebAssembly SPA hosted separately from the server
* Communicates with Elsa Server via REST APIs
* Receives real-time updates via SignalR connections
* Modular plugin architecture for extensibility

**Key Packages:**

* `Elsa.Studio` - Core studio infrastructure
* `Elsa.Studio.Core.BlazorWasm` - Blazor WASM hosting
* `Elsa.Studio.Workflows` - Workflow design module
* `Elsa.Api.Client` - API client for server communication

### 3. Activities

**Activities** are the building blocks of workflows, representing discrete units of work that can be composed together to create complex processes.

**Activity Types:**

1. **Control Flow Activities**: Sequence, Flowchart, If, Switch, For, While, Fork
2. **Data Activities**: SetVariable, WriteLine, ReadLine
3. **HTTP Activities**: HttpEndpoint, WriteHttpResponse, SendHttpRequest
4. **Blocking Activities**: Event, Delay, Timer (create bookmarks)
5. **Trigger Activities**: HttpEndpoint, Timer, Cron (can start workflows)
6. **Custom Activities**: User-defined activities extending base classes

**Activity Lifecycle:**

```
┌──────────────┐
│  Scheduled   │ ← Activity added to scheduler
└──────┬───────┘
       │
       ▼
┌──────────────┐
│  Executing   │ ← Activity.ExecuteAsync() called
└──────┬───────┘
       │
       ▼
┌──────────────┐   Creates    ┌──────────────┐
│  Suspending  │──Bookmark──→ │  Suspended   │
└──────┬───────┘              └──────────────┘
       │                              │
       │                              │ Resume with stimulus
       │                              ▼
       │                       ┌──────────────┐
       │                       │  Resuming    │
       │                       └──────┬───────┘
       │                              │
       ▼                              ▼
┌──────────────┐              ┌──────────────┐
│  Completed   │◄─────────────│  Completing  │
└──────────────┘              └──────────────┘
```

**Activity Properties:**

* **Inputs**: Configurable properties that accept values or expressions
* **Outputs**: Data produced by the activity for downstream consumption
* **Outcomes**: Named execution paths (e.g., "Done", "True", "False")
* **Metadata**: Descriptive information (display name, description, category, icon)

### 4. Workflows

Workflows in Elsa exist as both **definitions** (blueprints) and **instances** (executions).

#### Workflow Definitions

A **Workflow Definition** is a blueprint that describes:

* The activities and their configuration
* Connections between activities (execution flow)
* Variables and their types
* Input and output definitions
* Trigger configurations

**Storage:**

* Stored as JSON in the database
* Can be versioned (multiple versions of the same workflow)
* Support for draft and published states

#### Workflow Instances

A **Workflow Instance** represents an execution of a workflow definition:

* Contains the current execution state
* Stores variable values and execution history
* Maintains bookmarks for suspended execution
* Tracks correlation IDs for external system integration

**Instance States:**

* `Running` - Currently executing
* `Suspended` - Paused, waiting for external event or condition
* `Finished` - Completed successfully
* `Faulted` - Terminated due to an error
* `Canceled` - Manually or programmatically canceled

### 5. Persistence Layer

The **Persistence Layer** provides abstractions and implementations for storing workflow data.

**Storage Providers:**

* **Entity Framework Core**: SQL Server, PostgreSQL, SQLite, MySQL
* **MongoDB**: Document-based storage
* **Memory**: In-memory storage for testing

**Persisted Data:**

| Store Type                | Data Stored                                   | Purpose                        |
| ------------------------- | --------------------------------------------- | ------------------------------ |
| Workflow Definition Store | Workflow blueprints, versions, metadata       | Define workflow templates      |
| Workflow Instance Store   | Running/completed instances, state, variables | Track execution state          |
| Trigger Store             | Trigger definitions, bookmarks                | Enable workflow activation     |
| Activity Execution Store  | Activity execution records                    | Audit and debugging            |
| Execution Log Store       | Detailed execution logs                       | Troubleshooting and monitoring |

**Configuration Example:**

```csharp
elsa.UseWorkflowManagement(management =>
{
    management.UseEntityFrameworkCore(ef => 
        ef.UsePostgreSql(connectionString));
});

elsa.UseWorkflowRuntime(runtime =>
{
    runtime.UseEntityFrameworkCore(ef => 
        ef.UsePostgreSql(connectionString));
});
```

## Execution Model

Understanding Elsa's execution model is critical for architects and integrators.

### Workflow Execution Flow

```
┌─────────────────┐
│  Trigger Event  │ (HTTP request, timer, external event)
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ Trigger Matcher │ ← Finds workflows with matching triggers
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ Create Instance │ ← New WorkflowInstance created
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ Schedule Root   │ ← Root activity added to scheduler
│    Activity     │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ Execute Burst   │ ← Continuous execution until blocking
└────────┬────────┘
         │
         ├─────────────┐
         │             │
         ▼             ▼
┌─────────────┐  ┌──────────────┐
│  Complete   │  │  Suspended   │ ← Bookmark created
└─────────────┘  └──────┬───────┘
                        │
                        │ External stimulus
                        ▼
                 ┌──────────────┐
                 │ Resume Burst │
                 └──────┬───────┘
                        │
                        ▼
                 (Continue execution)
```

### Execute vs Dispatch

Elsa provides two primary mechanisms for starting workflows:

#### Execute

**Synchronous, inline execution** within the current context.

**Characteristics:**

* Runs in the caller's context (same thread, transaction)
* Blocks until completion or first suspension point
* Returns workflow state immediately
* Useful for short-lived workflows
* Better for unit testing

**Use Cases:**

* Workflows that complete in a single burst
* Synchronous business logic
* Testing scenarios

**Code Example:**

```csharp
var workflowRunner = serviceProvider.GetRequiredService<IWorkflowRunner>();
var result = await workflowRunner.RunAsync(workflow);
```

#### Dispatch

**Asynchronous, background execution** via the workflow runtime.

**Characteristics:**

* Queues workflow for background execution
* Returns immediately without waiting for completion
* Executes via mediator/message queue
* Supports distributed processing
* Better for long-running workflows

**Use Cases:**

* Long-running workflows with delays or external events
* High-throughput scenarios
* Distributed systems
* Fire-and-forget execution

**Code Example:**

```csharp
var workflowDispatcher = serviceProvider.GetRequiredService<IWorkflowDispatcher>();
await workflowDispatcher.DispatchAsync(new DispatchWorkflowDefinitionRequest
{
    DefinitionId = "my-workflow"
});
```

**Comparison Table:**

| Aspect             | Execute              | Dispatch               |
| ------------------ | -------------------- | ---------------------- |
| **Execution Mode** | Synchronous          | Asynchronous           |
| **Context**        | Caller's thread      | Background worker      |
| **Response**       | Waits for completion | Immediate return       |
| **Distribution**   | Single process       | Can be distributed     |
| **Performance**    | Lower overhead       | Higher throughput      |
| **Use Case**       | Short workflows      | Long-running workflows |

### Bookmarks, Triggers, and Stimuli

These three concepts work together to enable event-driven, long-running workflows.

#### Bookmarks

**Suspension points** that allow workflows to pause and resume later.

**How They Work:**

1. A blocking activity creates a bookmark with a unique payload
2. The workflow instance is suspended and persisted
3. Execution stops at that point
4. Later, an external event provides a matching stimulus
5. The bookmark is matched, and execution resumes

**Example:**

```csharp
public class WaitForApprovalActivity : Activity
{
    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
    {
        // Create a bookmark with a payload
        context.CreateBookmark(
            new Bookmark("approval-required", 
                         payload: new { ApprovalId = "12345" }));
        await Task.CompletedTask;
    }
}
```

#### Triggers

**Entry points** that can automatically start new workflow instances.

**Characteristics:**

* Activities marked with `ActivityKind.Trigger`
* Indexed when workflow definitions are published
* Matched against incoming events/stimuli
* Can start multiple instances (one per event)

**Common Triggers:**

* `HttpEndpoint` - HTTP requests at specific paths
* `Timer` - Time-based schedules
* `Cron` - Cron expressions
* `Event` - Custom application events

**How Triggers Work:**

```
1. Workflow published with HttpEndpoint trigger
2. Runtime indexes trigger: POST /api/orders
3. HTTP request arrives: POST /api/orders
4. Runtime matches trigger to workflow definition
5. New workflow instance created and started
6. Request data passed as workflow input
```

#### Stimuli

**External events** that either start workflows (via triggers) or resume them (via bookmarks).

**Types:**

* **Workflow Stimuli**: Start new instances (matched to triggers)
* **Bookmark Stimuli**: Resume suspended instances (matched to bookmarks)

**Flow:**

```
External Event → Stimulus → Trigger Matcher → Start Workflow
                         ↘ Bookmark Matcher → Resume Workflow
```

**Example - Resume via Stimulus (New Client API):**

```csharp
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Messages;

var workflowRuntime = serviceProvider.GetRequiredService<IWorkflowRuntime>();

// Create a workflow client for the specific suspended instance
var client = await workflowRuntime.CreateClientAsync(workflowInstanceId);

// Resume the workflow by running the instance with input that matches the bookmark
await client.RunInstanceAsync(new RunWorkflowInstanceRequest
{
    Input = new Dictionary<string, object>
    {
        ["Approved"] = true
    }
});
```

### Workflow Execution Internals

#### Activity Execution Pipeline

Activities execute through a configurable middleware pipeline:

```
Request → [Middleware 1] → [Middleware 2] → [Activity] → Response
            ↓                  ↓               ↓
         Logging           Validation      Execution
```

**Built-in Middleware:**

* Exception handling
* Logging
* Activity execution tracking
* State persistence
* Fault tolerance

#### Workflow Scheduler

The scheduler manages activity execution order:

**Scheduling Strategies:**

* **Sequential**: Activities execute one at a time
* **Parallel**: Multiple activities execute concurrently (Fork activity)

**Scheduler Operations:**

1. Add activities to the work queue
2. Dequeue next activity
3. Execute via activity pipeline
4. Handle outcomes (schedule next activities)
5. Repeat until queue is empty or suspended

#### State Management

Workflow state is managed through:

**Workflow Execution Context:**

* Contains current execution state
* Manages variables and their values
* Tracks activity execution history
* Maintains scheduler state

**Persistence Strategy:**

* State captured after each burst of execution
* Serialized to JSON
* Stored in database
* Restored on resume

## Data Flow

Understanding how data flows through the system is essential for integration.

### Request Flow (HTTP Trigger Example)

```
1. HTTP Request
   ↓
2. ASP.NET Core Middleware (app.UseWorkflows())
   ↓
3. Trigger Matcher → Finds workflows with matching HttpEndpoint
   ↓
4. Workflow Dispatcher → Queues workflow execution
   ↓
5. Workflow Runtime → Picks up message
   ↓
6. Workflow Execution Engine
   ↓
7. Activity Execution (HttpEndpoint)
   │  - Captures request data
   │  - Sets workflow variables
   ↓
8. Subsequent Activities
   │  - Access request data via variables
   │  - Process business logic
   ↓
9. WriteHttpResponse Activity
   │  - Generates HTTP response
   ↓
10. HTTP Response sent to client
```

### Variable and Output Flow

```
┌──────────────┐
│  Activity A  │ Produces output → Stored in execution context
└──────┬───────┘
       │
       ▼ Output bound to variable
┌──────────────┐
│  Variable    │ Available to downstream activities
└──────┬───────┘
       │
       ▼ Variable accessed
┌──────────────┐
│  Activity B  │ Consumes variable as input
└──────────────┘
```

**Example:**

```csharp
var queryStringsVar = builder.WithVariable<IDictionary<string, object>>();
var messageVar = builder.WithVariable<string>();

builder.Root = new Sequence
{
    Activities =
    {
        // Activity A: Capture HTTP query strings
        new HttpEndpoint
        {
            Path = new("/hello"),
            QueryStringData = new(queryStringsVar)
        },
        // Transform data
        new SetVariable
        {
            Variable = messageVar,
            Value = new(context => 
                queryStringsVar.Get(context)!["message"].ToString())
        },
        // Activity B: Use the variable
        new WriteHttpResponse
        {
            Content = new(messageVar)
        }
    }
};
```

## Scalability and Performance

Elsa is designed for high performance and horizontal scalability.

### Performance Characteristics

**Execution Speed:**

* In-memory activities: \~1-5ms per activity
* Persistence overhead: \~10-50ms per burst
* HTTP activities: Depends on external service latency

**Throughput:**

* Single instance: 100-1000+ workflows/second (depends on workflow complexity)
* Clustered: Linear scaling with additional nodes

**Memory:**

* Base process: \~100-200MB
* Per active workflow: \~1-5KB (suspended workflows minimal overhead)
* Workflow definitions cached in memory

### Horizontal Scaling

Elsa supports running multiple server instances for high availability and scalability.

**Required Configuration for Clustering:**

1. **Distributed Runtime**

```csharp
elsa.UseWorkflowRuntime(runtime =>
{
    runtime.UseDistributedRuntime();
});
```

2. **Distributed Locking**

```csharp
runtime.DistributedLockProvider = sp => 
    new PostgresDistributedSynchronizationProvider(connectionString);
```

3. **Distributed Caching**

```csharp
elsa.UseDistributedCache(cache =>
{
    cache.UseMassTransit();
});

elsa.UseMassTransit(mt =>
{
    mt.UseRabbitMq(rabbitMqConnectionString);
});
```

4. **Quartz.NET Clustering** (if using Quartz scheduler)

```csharp
elsa.UseQuartz(quartz =>
{
    quartz.UsePostgreSql(connectionString);
});
```

**Scaling Strategies:**

| Strategy               | Description                       | Use Case                              |
| ---------------------- | --------------------------------- | ------------------------------------- |
| **Vertical Scaling**   | Increase CPU/memory, worker count | Initial scaling, cost-effective       |
| **Horizontal Scaling** | Add more server instances         | High throughput, high availability    |
| **Read Replicas**      | Separate read/write databases     | Read-heavy workloads                  |
| **Partitioning**       | Route workflows to specific nodes | Tenant isolation, resource management |

### Optimization Tips

1. **Reduce Persistence Overhead**
   * Disable logging for high-frequency activities
   * Use in-memory storage for transient workflows
   * Batch database operations
2. **Optimize Activity Design**
   * Keep activities lightweight
   * Avoid blocking I/O in synchronous activities
   * Use async patterns for I/O operations
3. **Worker Configuration**

```csharp
builder.Services.Configure<MediatorOptions>(opt =>
{
    opt.CommandWorkerCount = 16;
    opt.JobWorkerCount = 16;
    opt.NotificationWorkerCount = 16;
});
```

4. **Database Optimization**
   * Add appropriate indexes
   * Regular maintenance (vacuum, statistics)
   * Connection pooling
5. **Cache Workflow Definitions**
   * Enabled by default
   * Reduces database queries
   * Invalidated on changes via distributed cache

## Extensibility Points

Elsa provides numerous extension points for customization.

### 1. Custom Activities

Create domain-specific activities by implementing `IActivity` or inheriting from base classes:

```csharp
[Activity("MyCategory", "My Activity", "Does something custom")]
public class MyCustomActivity : CodeActivity
{
    [Input(Description = "The input value")]
    public Input<string> InputValue { get; set; } = default!;

    [Output(Description = "The output value")]
    public Output<string> OutputValue { get; set; } = default!;

    protected override async ValueTask ExecuteAsync(
        ActivityExecutionContext context)
    {
        var input = context.Get(InputValue);
        var result = await Task.FromResult(input.ToUpper());
        context.Set(OutputValue, result);
    }
}
```

### 2. Custom Triggers

Implement custom trigger activities for event-driven workflows:

```csharp
[Activity("MyCategory", "My Trigger", Kind = ActivityKind.Trigger)]
public class MyTriggerActivity : Activity<object>
{
    protected override async ValueTask ExecuteAsync(
        ActivityExecutionContext context)
    {
        if (!context.IsTriggerOfWorkflow())
        {
            // Create bookmark for existing instances
            context.CreateBookmark();
            return;
        }
        
        // Trigger mode: complete immediately
        await context.CompleteActivityAsync();
    }

    protected override object GetTriggerPayload(
        TriggerIndexingContext context)
    {
        return new { EventType = "MyEvent" };
    }
}
```

### 3. Custom Middleware

Add custom behavior to workflow or activity execution:

```csharp
public class MyActivityMiddleware : IActivityExecutionMiddleware
{
    public async ValueTask ExecuteAsync(
        ActivityExecutionContext context, 
        ActivityExecutionDelegate next)
    {
        // Before execution
        Console.WriteLine($"Executing {context.Activity.Type}");
        
        await next(context);
        
        // After execution
        Console.WriteLine($"Completed {context.Activity.Type}");
    }
}

// Register middleware
elsa.UseWorkflowRuntime(runtime =>
{
    runtime.UseDefaultActivityExecutionPipeline(pipeline =>
    {
        pipeline.UseMiddleware<MyActivityMiddleware>();
    });
});
```

### 4. Custom Persistence Providers

Implement custom storage backends:

```csharp
public class MyWorkflowInstanceStore : IWorkflowInstanceStore
{
    public Task SaveAsync(WorkflowInstance instance, 
        CancellationToken cancellationToken) { ... }
    
    public Task<WorkflowInstance?> FindAsync(
        WorkflowInstanceFilter filter, 
        CancellationToken cancellationToken) { ... }
    
    // ... other interface methods
}
```

### 5. Custom Expression Evaluators

Add support for custom expression languages:

```csharp
public class MyExpressionHandler : IExpressionHandler
{
    public string Language => "mylang";

    public Task<object?> EvaluateAsync(
        Expression expression,
        Type returnType,
        ExpressionExecutionContext context,
        CancellationToken cancellationToken) { ... }
}
```

### 6. Studio Extensibility

**Custom UI Hints:** Define how activity properties appear in the designer:

```csharp
[Input(UIHint = InputUIHints.Dropdown)]
public Input<string> Status { get; set; } = default!;
```

**Custom Activity Providers:** Dynamically generate activities from external sources:

```csharp
public class ApiActivityProvider : IActivityProvider
{
    public async ValueTask<IEnumerable<ActivityDescriptor>> 
        GetDescriptorsAsync(CancellationToken cancellationToken)
    {
        // Fetch activity definitions from API
        var activities = await _apiClient.GetActivitiesAsync();
        
        // Convert to ActivityDescriptor
        return activities.Select(a => new ActivityDescriptor
        {
            TypeName = a.Type,
            DisplayName = a.Name,
            // ... other properties
        });
    }
}
```

## Deployment Topologies

Elsa supports various deployment configurations to meet different requirements.

### 1. All-in-One (Development)

Single server hosting both Elsa Server and Studio.

```
┌─────────────────────────┐
│   Single Server         │
│  ┌──────────────────┐   │
│  │  Elsa Server     │   │
│  │  + Studio WASM   │   │
│  └──────────────────┘   │
│  ┌──────────────────┐   │
│  │  Database        │   │
│  └──────────────────┘   │
└─────────────────────────┘
```

**Pros:** Simple setup, minimal infrastructure **Cons:** Not scalable, single point of failure **Use Case:** Development, POC, small deployments

### 2. Separate Server and Studio (Recommended)

Elsa Server and Studio deployed as separate applications.

```
┌─────────────────┐       ┌─────────────────┐
│  Elsa Studio    │──────▶│  Elsa Server    │
│  (Blazor WASM)  │  API  │  (ASP.NET Core) │
└─────────────────┘       └────────┬────────┘
                                   │
                          ┌────────▼────────┐
                          │    Database     │
                          └─────────────────┘
```

**Pros:** Independent scaling, separate concerns **Cons:** More complex deployment **Use Case:** Production deployments

### 3. Multi-Instance Cluster (High Availability)

Multiple Elsa Server instances behind a load balancer.

```
┌─────────────────┐       ┌─────────────────┐
│  Elsa Studio    │       │  Load Balancer  │
└────────┬────────┘       └────────┬────────┘
         │                         │
         │        ┌────────────────┼────────────────┐
         │        │                │                │
         │   ┌────▼─────┐    ┌────▼─────┐    ┌────▼─────┐
         └──▶│ Server 1 │    │ Server 2 │    │ Server N │
             └────┬─────┘    └────┬─────┘    └────┬─────┘
                  │               │               │
                  └───────┬───────┴───────┬───────┘
                          │               │
                   ┌──────▼──────┐ ┌─────▼──────┐
                   │  Database   │ │ Redis/     │
                   │  (Primary)  │ │ RabbitMQ   │
                   └─────────────┘ └────────────┘
```

**Requirements:**

* Shared database
* Distributed locking (PostgreSQL, Redis)
* Distributed caching (MassTransit + RabbitMQ/Azure Service Bus)
* Quartz.NET clustering (if using Quartz)

**Pros:** High availability, horizontal scaling **Cons:** Complex configuration, infrastructure overhead **Use Case:** Production, high-traffic environments

### 4. Kubernetes Deployment

Containerized deployment with orchestration.

```
┌────────────────────────────────────────────────────┐
│                  Kubernetes Cluster                │
│                                                    │
│  ┌──────────────────────────────────────────────┐ │
│  │              Ingress Controller              │ │
│  └────────┬─────────────────────┬────────────────┘ │
│           │                     │                  │
│  ┌────────▼────────┐   ┌───────▼──────────┐      │
│  │  Studio Pod(s)  │   │  Server Pod(s)   │      │
│  │  (Deployment)   │   │  (Deployment)    │      │
│  └─────────────────┘   └───────┬──────────┘      │
│                                 │                  │
│  ┌──────────────────────────────▼────────────┐   │
│  │         External Services               │   │
│  │  - PostgreSQL (StatefulSet/External)    │   │
│  │  - RabbitMQ (StatefulSet/External)      │   │
│  │  - Redis (StatefulSet/External)         │   │
│  └─────────────────────────────────────────┘   │
└────────────────────────────────────────────────────┘
```

**Key Considerations:**

* Use persistent volumes for workflow state
* Configure health checks and readiness probes
* Set up horizontal pod autoscaling
* Use ConfigMaps and Secrets for configuration
* Enable distributed runtime features

**Example Deployment YAML:**

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: elsa-server
spec:
  replicas: 3
  selector:
    matchLabels:
      app: elsa-server
  template:
    metadata:
      labels:
        app: elsa-server
    spec:
      containers:
      - name: elsa-server
        image: myregistry/elsa-server:latest
        ports:
        - containerPort: 80
        env:
        - name: ConnectionStrings__Default
          valueFrom:
            secretKeyRef:
              name: elsa-secrets
              key: database-connection
        livenessProbe:
          httpGet:
            path: /health
            port: 80
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 80
          initialDelaySeconds: 10
          periodSeconds: 5
```

### 5. Microservices Architecture

Separate workflow domain into multiple services.

```
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│  Order      │    │  Inventory  │    │  Shipping   │
│  Workflows  │    │  Workflows  │    │  Workflows  │
│  Service    │    │  Service    │    │  Service    │
└──────┬──────┘    └──────┬──────┘    └──────┬──────┘
       │                  │                  │
       └──────────┬───────┴───────┬──────────┘
                  │               │
          ┌───────▼──────┐ ┌─────▼──────┐
          │  Message Bus │ │  Database  │
          │  (RabbitMQ)  │ │  (Per Svc) │
          └──────────────┘ └────────────┘
```

**Use Case:** Domain-driven design, team autonomy **Pros:** Independent deployment, domain isolation **Cons:** Complexity, distributed transactions

## Multi-Tenancy Architecture

Elsa supports multi-tenancy for SaaS applications.

### Tenant Isolation Strategies

**1. Shared Database, Shared Schema**

* All tenants share the same database and tables
* Tenant ID column on all tables
* Row-level security via application logic

**2. Shared Database, Separate Schemas**

* Each tenant gets their own schema
* Better isolation than shared schema
* More complex migration management

**3. Separate Databases**

* Complete database isolation per tenant
* Best security and isolation
* Higher resource overhead

### Multi-Tenant Configuration

```csharp
builder.Services.AddElsa(elsa =>
{
    elsa.UseMultitenancy(multitenancy =>
    {
        multitenancy.UseEntityFrameworkCoreStore();
    });
    
    elsa.UseWorkflowManagement(management =>
    {
        management.UseEntityFrameworkCore(ef =>
        {
            ef.UseMultitenancy();
        });
    });
});
```

### Tenant Resolution

Tenants are resolved via:

* HTTP headers
* Subdomain
* URL path
* Claims in JWT token
* Custom resolution strategy

```csharp
public class CustomTenantResolver : ITenantResolver
{
    public Task<TenantResolverResult> ResolveAsync(
        TenantResolverContext context)
    {
        var httpContext = context.HttpContext;
        var tenantId = httpContext.Request.Headers["X-Tenant-Id"];
        
        return Task.FromResult(new TenantResolverResult
        {
            TenantId = tenantId
        });
    }
}
```

## Security Considerations

### Authentication & Authorization

**Server Authentication:**

* API Key authentication for machine-to-machine
* JWT bearer tokens for user authentication
* OIDC integration (Azure AD, Auth0, IdentityServer)

**Studio Authentication:**

* Login module with username/password
* OIDC integration
* Custom authentication providers

**Configuration:**

```csharp
elsa.UseIdentity(identity =>
{
    identity.TokenOptions = options => 
        options.SigningKey = builder.Configuration["Jwt:SigningKey"]; // Use secure key storage in production
    identity.UseAdminUserProvider();
});

elsa.UseDefaultAuthentication(auth => 
    auth.UseAdminApiKey());
```

### Workflow Security

**HTTP Endpoint Authorization:**

```csharp
new HttpEndpoint
{
    Path = new("/secure-endpoint"),
    AuthorizeWithPolicy = new("AdminOnly"),
    CanStartWorkflow = true
}
```

**Variable and Input Validation:**

* Validate all external inputs
* Sanitize data before persistence
* Use typed variables to prevent injection

## Monitoring and Observability

### Health Checks

```csharp
builder.Services.AddHealthChecks()
    .AddDbContextCheck<WorkflowDbContext>()
    .AddRabbitMQ(rabbitMqConnectionString);

app.MapHealthChecks("/health");
```

### Logging

Elsa uses structured logging via `ILogger`:

```csharp
builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowRuntime(runtime =>
    {
        runtime.LogPersistenceMode = LogPersistenceMode.Inherit;
    });
});
```

**Log Levels:**

* Activity execution logs
* Workflow lifecycle events
* Exception details
* Performance metrics

### Execution Logs

Activity execution is logged to the database for auditing:

```csharp
var executionLogs = await workflowExecutionLogStore.FindAsync(
    new WorkflowExecutionLogRecordFilter
    {
        WorkflowInstanceId = instanceId
    });
```

### Metrics and Telemetry

**Key Metrics:**

* Workflows started/completed per second
* Average workflow execution time
* Active workflow instances
* Suspended workflow count
* Activity failure rate

**Integration:**

* Application Insights
* Prometheus
* OpenTelemetry

## Best Practices

### Design Patterns

1. **Idempotency**: Design workflows to handle duplicate executions
2. **Compensation**: Implement compensating actions for rollback scenarios
3. **Saga Pattern**: Use for distributed transactions across services
4. **State Machine**: Use StateMachine activities for complex state transitions

### Performance

1. **Minimize Persistence**: Disable logging for high-frequency activities
2. **Batch Operations**: Group related activities to reduce overhead
3. **Async Activities**: Use async patterns for I/O operations
4. **Connection Pooling**: Configure database connection pools appropriately

### Reliability

1. **Error Handling**: Use Fault activity and exception handling
2. **Retry Logic**: Implement retry policies for transient failures
3. **Timeouts**: Set appropriate timeouts for external calls
4. **Health Checks**: Monitor system health continuously

### Maintenance

1. **Versioning**: Version workflow definitions for safe updates
2. **Migration**: Plan for workflow instance migration when updating definitions
3. **Archival**: Archive completed workflows periodically
4. **Monitoring**: Set up alerts for failures and performance degradation

## Reference Sources

This architecture guide is based on the following official Elsa sources:

**Elsa Core Repository:**

* <https://github.com/elsa-workflows/elsa-core>
* Core workflow engine implementation
* Activity library and runtime services
* Persistence layer abstractions

**Elsa Studio Repository:**

* <https://github.com/elsa-workflows/elsa-studio>
* Blazor WebAssembly designer application
* Studio modules and extensibility

**Official Documentation:**

* [Getting Started Guides](https://github.com/elsa-workflows/elsa-gitbook/blob/main/getting-started/README.md)
* [Application Types](https://github.com/elsa-workflows/elsa-gitbook/blob/main/application-types/README.md)
* [Distributed Hosting](/hosting/distributed-hosting)
* [Custom Activities](/extensibility/custom-activities)

## Next Steps

* **For Architects**: Review [Distributed Hosting](/hosting/distributed-hosting) for production deployment
* **For Backend Integrators**: Learn to create [Custom Activities](/extensibility/custom-activities)
* **For Platform/DevOps**: Explore [Container Deployment](/getting-started/containers) options
* **For Workflow Designers**: Start with [Hello World](/getting-started/hello-world) tutorial

## Summary

Elsa Workflows provides a flexible, scalable architecture for building workflow-driven applications. Key takeaways:

* **Modular Design**: Separate concerns across layers (presentation, application, runtime, persistence)
* **Extensible**: Custom activities, triggers, middleware, and persistence providers
* **Scalable**: Horizontal scaling with distributed runtime and locking
* **Event-Driven**: Triggers, bookmarks, and stimuli enable long-running workflows
* **Flexible Deployment**: From single-server to Kubernetes clusters
* **Production-Ready**: Multi-tenancy, security, monitoring, and high availability

By understanding these architectural principles, you can design and deploy robust workflow solutions with Elsa.


# Hello World

In this topic, we'll setup a simple Console and an ASP.NET Core application that can host and execute workflows.

## Console <a href="#setup" id="setup"></a>

{% stepper %}
{% step %}
**Create Console App**

Start by creating a new console application:

```bash
dotnet new console -n "ElsaConsole"
```

{% endstep %}

{% step %}
**Add Packages**

Navigate to your newly created project's root directory and add the following packages:

```bash
cd ElsaConsole
dotnet add package Elsa
```

{% endstep %}

{% step %}
**Modify Program.cs**

Open `Program.cs` and replace its contents with the following:

```csharp
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Microsoft.Extensions.DependencyInjection;

// Setup service container.
var services = new ServiceCollection();

// Add Elsa services to the container.
services.AddElsa();

// Build the service container.
var serviceProvider = services.BuildServiceProvider();

// Define a simple workflow with multiple activities.
var workflow = new Sequence
{
    Activities =
    {
        new WriteLine("Hello World!"),
        new WriteLine("We can do more than a one-liner!")
    }
};

// Resolve a workflow runner to execute the workflow.
var workflowRunner = serviceProvider.GetRequiredService<IWorkflowRunner>();

// Run the workflow.
await workflowRunner.RunAsync(workflow);
```

This code sets up a service container and adds Elsa services to it. The `serviceProvider` can be used to resolve Elsa services and run workflows.
{% endstep %}
{% endstepper %}

## ASP.NET Core

> **Note:** The ASP.NET Core sample below may require updates to work with the latest Elsa 3.2+/3.3 versions. For a fully working sample compatible with the current version of Elsa, please refer to the [Elsa Samples repository](https://github.com/elsa-workflows/elsa-samples).

{% stepper %}
{% step %}
**Create the Project**

Create a new empty ASP.NET app using the following command:

```bash
dotnet new web -n "ElsaWeb"
```

{% endstep %}

{% step %}
**Add Packages**

Navigate to your project's root directory and install the Elsa package:

```bash
cd ElsaWeb
dotnet add package Elsa
dotnet add package Elsa.Http
```

{% endstep %}

{% step %}
**Modify Program.cs**

Open `Program.cs` in your project and replace its contents with the code provided below.

**Program.cs**

```csharp
using Elsa.Extensions;
using ElsaWeb.Workflows;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddElsa(elsa =>
{
    elsa.AddWorkflow<HttpHelloWorld>();
    elsa.UseHttp(http => http.ConfigureHttpOptions = options =>
    {
        options.BaseUrl = new Uri("https://localhost:5001");
        options.BasePath = "/workflows";
    });
});

var app = builder.Build();

// Configure the HTTP request pipeline.
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.UseWorkflows();
app.Run();
```

{% endstep %}

{% step %}
**Add HttpHelloWorld Workflow**

Create a new directory called `Workflows` and add a new file to it called `HttpHelloWorld.cs` with the following.

**Workflows/HttpHelloWorld.cs**

```csharp
using Elsa.Http;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Contracts;

namespace ElsaWeb.Workflows;

public class HttpHelloWorld : WorkflowBase
{
    protected override void Build(IWorkflowBuilder builder)
    {
        var queryStringsVariable = builder.WithVariable<IDictionary<string, object>>();
        var messageVariable = builder.WithVariable<string>();

        builder.Root = new Sequence
        {
            Activities =
            {
                new HttpEndpoint
                {
                    Path = new("/hello-world"),
                    CanStartWorkflow = true,
                    QueryStringData = new(queryStringsVariable)
                },
                new SetVariable
                {
                    Variable = messageVariable,
                    Value = new(context =>
                    {
                        var queryStrings = queryStringsVariable.Get(context)!;
                        var message = queryStrings.TryGetValue("message", out var messageValue) ? messageValue.ToString() : "Hello world of HTTP workflows!";
                        return message;
                    })
                },
                new WriteHttpResponse
                {
                    Content = new(messageVariable)
                }
            }
        };
    }
}
```

{% endstep %}
{% endstepper %}

## Summary <a href="#summary" id="summary"></a>

This document explains setting up Console and ASP.NET Core apps using Elsa workflows. For the Console app, we configured a service container, added Elsa, and ran a "Hello World" workflow. The ASP.NET Core app integrates Elsa with HTTP endpoints to process workflows. Follow the code samples for package additions and `Program.cs` configurations. Refer to source code links for further details.

## Source Code

* [Console app](https://github.com/elsa-workflows/elsa-guides/tree/main/src/installation/elsa-console)
* [ASP.NET Core app](https://github.com/elsa-workflows/elsa-guides/tree/main/src/installation/elsa-web)


# Prerequisites

Before you can start creating your own workflow server using Elsa, there are a few prerequisites to consider.

### .NET <a href="#dotnet" id="dotnet"></a>

* .NET SDK (Version 8 or higher)
* A code editor (e.g., Visual Studio, Visual Studio Code, Rider)
* Basic knowledge of C# and ASP.NET Core and optionally Blazor for extending Elsa Studio

### Docker <a href="#docker" id="docker"></a>

In order to be able to run the various prebuilt Docker images hosting Elsa Server, Elsa Studio and Elsa Server + Studio, you will need to have [Docker for Desktop](https://www.docker.com/products/docker-desktop/) (or a similar tool) installed.


# Packages

Elsa is available as a collection of NuGet packages. Some packages are required for executing workflows, while others provide integrations with systems like service buses, cloud services, and additional features such as email delivery.

## **Main Package**

The primary package you'll need to get started with Elsa is the `Elsa` package. It's a bundle that includes the following essential packages:

* Elsa.Api.Common
* Elsa.Mediator
* Elsa.Workflows.Core
* Elsa.Workflows.Management
* Elsa.Workflows.Runtime

To install the core `Elsa` package, use the `dotnet` CLI:

```
dotnet add package Elsa
```

## **Package Feeds**

Elsa packages are distributed through various feeds based on their stability and release phase:

<table><thead><tr><th width="228">Type</th><th width="100">Feed</th><th>URL</th></tr></thead><tbody><tr><td>Releases</td><td>NuGet</td><td>https://api.nuget.org/v3/index.json</td></tr><tr><td>Release Candidates</td><td>NuGet</td><td>https://api.nuget.org/v3/index.json</td></tr><tr><td>Previews</td><td>Feedz</td><td>https://f.feedz.io/elsa-workflows/elsa-3/nuget/index.json</td></tr></tbody></table>

### **Releases** <a href="#releases" id="releases"></a>

Stable versions of Elsa are distributed via NuGet.org.

### **Release Candidates (RC)** <a href="#release-candidates-rc" id="release-candidates-rc"></a>

RC packages are also available on NuGet.org. They offer a sneak peek into upcoming features, allowing users to test and provide feedback before the final release. While RC packages are generally stable, they might still undergo changes before the final release.

### **Previews** <a href="#previews" id="previews"></a>

Preview versions represent the cutting-edge developments in Elsa. They are automatically built and deployed to a public feed on Feedz whenever changes are pushed to the `v3` branch. While they provide the latest features and fixes, they might introduce breaking changes.

To access preview packages, include the feed URL when using the dotnet CLI or add it to your `NuGet.config`:

```xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="NuGet official package source" value="https://api.nuget.org/v3/index.json" />
    <add key="Elsa 3 preview" value="https://f.feedz.io/elsa-workflows/elsa-3/nuget/index.json" />
  </packageSources>
</configuration>
```

{% hint style="warning" %}
**Preview Packages**

Ensure the "Preview" checkbox is ticked in your NuGet explorer to view the preview packages.
{% endhint %}

## **Versioning Strategy** <a href="#versioning-strategy" id="versioning-strategy"></a>

Elsa uses to the following versioning strategy:

* **Released** packages: Major.Minor.Revision (e.g., `3.0.1`)
* **Release Candidate** packages: Major.Minor.Revision-rcX (e.g., `3.0.2-rc1`)
* **Preview** packages: Major.Minor.Revision-preview\.X (e.g., `3.0.2-preview.128`)

The major version remains consistent unless significant changes occur. New features increment the minor version, while fixes or minor improvements bump the revision number.\\


# Database Configuration

Learn how to configure Elsa Workflows to use different database providers for persistence, including SQL Server, PostgreSQL, and MongoDB.

This guide explains how to configure Elsa Workflows to use different database providers for storing workflow definitions, instances, and execution data. Elsa supports multiple database backends through Entity Framework Core (EF Core) and MongoDB.

## Supported Database Providers

Elsa supports the following database providers:

* **SQL Server** (recommended for production on Windows environments)
* **PostgreSQL** (recommended for production on Linux/Unix environments)
* **SQLite** (default, suitable for development and single-instance deployments)
* **MySQL/MariaDB** (supported but less commonly used)
* **MongoDB** (document database for specific use cases)

## Prerequisites

* Elsa Server project (see [Server Setup Guide](/application-types/elsa-server))
* Database server (local or remote)
* Appropriate NuGet packages installed

## Using SQL Server instead of SQLite

By default, Elsa uses SQLite for development scenarios. For production deployments, especially on Windows environments, SQL Server is recommended. The migration is straightforward:

1. **Install SQL Server packages:**

```bash
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Elsa.Persistence.EFCore.SqlServer
```

2. **Replace `UseSqlite()` with `UseSqlServer()`** in your `Program.cs`:

```csharp
builder.Services.AddElsa(elsa =>
{
    // Before: ef.UseSqlite()
    // After: ef.UseSqlServer() with connection string
    elsa.UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => 
        ef.UseSqlServer(builder.Configuration.GetConnectionString("SqlServer")!)));
    elsa.UseWorkflowRuntime(runtime => runtime.UseEntityFrameworkCore(ef => 
        ef.UseSqlServer(builder.Configuration.GetConnectionString("SqlServer")!)));
    elsa.UseWorkflowsApi();
});
```

3. **Update connection string** in `appsettings.json`:

```json
{
  "ConnectionStrings": {
    "SqlServer": "Server=localhost;Database=Elsa;User Id=sa;Password=YourPassword123;TrustServerCertificate=true"
  }
}
```

For more detailed information about persistence strategies, connection pooling, and advanced database configurations, see the [Persistence Guide](/guides/persistence). For a comprehensive SQL Server configuration guide including production considerations, troubleshooting, and migration strategies, see the [SQL Server Guide](/guides/persistence/sql-server).

## Configuring SQL Server

### 1. Install NuGet Packages

```bash
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Elsa.Persistence.EFCore.SqlServer
```

### 2. Configure Services

In `Program.cs`, add the following:

```csharp
builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => 
        ef.UseSqlServer(builder.Configuration.GetConnectionString("SqlServer")!)));
    elsa.UseWorkflowRuntime(runtime => runtime.UseEntityFrameworkCore(ef => 
        ef.UseSqlServer(builder.Configuration.GetConnectionString("SqlServer")!)));
    elsa.UseWorkflowsApi();
});
```

### 3. Connection String

Add to `appsettings.json`:

```json
{
  "ConnectionStrings": {
    "SqlServer": "Server=localhost;Database=Elsa;User Id=sa;Password=YourPassword123;TrustServerCertificate=true"
  }
}
```

## Configuring PostgreSQL

### 1. Install NuGet Packages

```bash
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Elsa.Persistence.EFCore.PostgreSql
```

### 2. Configure Services

In `Program.cs`:

```csharp
builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => 
        ef.UsePostgreSql(builder.Configuration.GetConnectionString("PostgreSql")!)));
    elsa.UseWorkflowRuntime(runtime => runtime.UseEntityFrameworkCore(ef => 
        ef.UsePostgreSql(builder.Configuration.GetConnectionString("PostgreSql")!)));
    elsa.UseWorkflowsApi();
});
```

### 3. Connection String

```json
{
  "ConnectionStrings": {
    "PostgreSql": "Host=localhost;Database=elsa;Username=elsa;Password=elsa;Port=5432"
  }
}
```

## Configuring MongoDB

### 1. Install NuGet Packages

```bash
dotnet add package Elsa.Persistence.MongoDb
```

### 2. Configure Services

In `Program.cs`:

```csharp
var mongoConnectionString = builder.Configuration.GetConnectionString("MongoDb")!;

builder.Services.AddElsa(elsa =>
{
    elsa.UseMongoDb(mongoConnectionString);
    elsa.UseWorkflowManagement(management => management.UseMongoDb());
    elsa.UseWorkflowRuntime(runtime => runtime.UseMongoDb());
    elsa.UseWorkflowsApi();
});
```

### 3. Connection String

```json
{
  "ConnectionStrings": {
    "MongoDb": "mongodb://localhost:27017/elsa"
  }
}
```

## Environment Variables

You can also configure database connections using environment variables:

```bash
# Database provider
DATABASEPROVIDER=PostgreSql

# Connection strings
CONNECTIONSTRINGS__SQLSERVER=Server=...;Database=...;...
CONNECTIONSTRINGS__POSTGRESQL=Host=...;Database=...;...
CONNECTIONSTRINGS__MONGODB=mongodb://.../...
```

## Running Migrations

For EF Core-based providers (SQL Server, PostgreSQL, SQLite), you need to run migrations. For detailed information about working with EF Core migrations, including custom entities and migration strategies, see the [EF Core Migrations Guide](/guides/persistence/ef-migrations).

### 1. Install EF Core Tools

```bash
dotnet tool install --global dotnet-ef
```

### 2. Apply Migrations

```bash
# For Management database
dotnet ef database update --context Elsa.Workflows.Management.Entities.ManagementDbContext

# For Runtime database
dotnet ef database update --context Elsa.Workflows.Runtime.Entities.RuntimeDbContext
```

### 3. Custom Migration Paths

If using separate databases, specify the connection string:

```bash
dotnet ef database update --context Elsa.Workflows.Management.Entities.ManagementDbContext --connection "YourManagementConnectionString"
```

## Multi-Database Scenarios

Elsa supports using separate databases for management (workflow definitions) and runtime (executions):

### Separate Databases Configuration

```csharp
builder.Services.AddElsa(elsa =>
{
    // Management database (definitions)
    elsa.UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => ef.UseSqlServer("ManagementConnectionString")));
    
    // Runtime database (executions)
    elsa.UseWorkflowRuntime(runtime => runtime.UseEntityFrameworkCore(ef => ef.UsePostgreSql("RuntimeConnectionString")));
});
```

### Benefits

* Scale management and runtime independently
* Use different database technologies for each
* Isolate sensitive runtime data

## Troubleshooting

### Common Issues

#### 1. Migration Errors

**Error:** "The term 'dotnet-ef' is not recognized"

**Solution:** Ensure EF Core tools are installed globally:

```bash
dotnet tool install --global dotnet-ef
```

#### 2. Connection Timeout

**Error:** "Timeout expired"

**Solutions:**

* Increase connection timeout in connection string: `;Timeout=60`
* Check database server availability
* Verify firewall settings

#### 3. Permission Denied

**Error:** "Login failed for user"

**Solutions:**

* Verify username/password
* Check user permissions on database
* Ensure database exists

#### 4. MongoDB Connection Issues

**Error:** "Unable to connect to server"

**Solutions:**

* Ensure MongoDB is running
* Check connection string format
* Verify authentication if enabled

### Logging

Enable detailed database logging:

```csharp
builder.Services.AddDbContext<ManagementDbContext>(options =>
    options.UseSqlServer(connectionString)
           .LogTo(Console.WriteLine, LogLevel.Information)
           .EnableSensitiveDataLogging());
```

## Production Considerations

### Performance Tuning

* Use connection pooling
* Configure appropriate connection limits
* Monitor query performance
* Consider database indexing

### Security

* Use strong passwords
* Enable SSL/TLS encryption
* Restrict database access to application servers
* Rotate credentials regularly

### Backup and Recovery

* Implement regular database backups
* Test restore procedures
* Plan for database failover scenarios

### Monitoring

* Monitor database performance metrics
* Set up alerts for connection issues
* Log database operations for auditing

## Next Steps

* [Docker Deployment](/getting-started/containers/docker-compose/persistent-database)
* [Authentication Setup](/guides/authentication)
* [Workflow Persistence](/guides/persistence)


# Containers

Elsa not only allows for the integration of its packages into custom .NET applications, but also offers prebuilt Docker images for ease of deployment. These images come in various configurations tailored to different needs, such as the standalone Elsa Server for API-based workflow execution, Elsa Studio for a user-friendly design interface, and a combined setup that integrates both Elsa Server and Studio.


# Docker

The Elsa project currently offers three different Docker images:

* [Elsa Server + Studio](https://hub.docker.com/repository/docker/elsaworkflows/elsa-server-and-studio-v3/general)
* [Elsa Server](https://hub.docker.com/repository/docker/elsaworkflows/elsa-server-v3/general)
* [Elsa Studio](https://hub.docker.com/repository/docker/elsaworkflows/elsa-studio-v3/general)

These images make it easy to give Elsa a quick spin without first creating an ASP.NET application and setting up Elsa. Before trying to run an image, [make sure you have Docker installed](https://elsa-workflows.github.io/elsa-documentation/prerequisites.html#docker) on your machine.

### Elsa Server + Studio <a href="#elsa-server-and-studio" id="elsa-server-and-studio"></a>

This image hosts an ASP.NET Core application that runs both as an Elsa Server as well as an Elsa Studio application. To run the container, simply run the following commands from your terminal:

```bash
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
```

When the container has started, open a web browser and navigate to [http://localhost:13000](http://localhost:13000/). On the login screen, enter the following credentials:

```shell-session
username: admin
password: password
```

### Elsa Server <a href="#elsa-server" id="elsa-server"></a>

This image hosts an ASP.NET Core application that runs as an Elsa Server. To run the container, simply run the following commands from your terminal:

```bash
docker pull elsaworkflows/elsa-server-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-v3:latest
```

When the container has started, open a web browser and navigate to [http://localhost:13000](http://localhost:13000/).

To view the API endpoints, navigate to <http://localhost:13000/swagger>.

### Elsa Studio <a href="#elsa-studio" id="elsa-studio"></a>

This image hosts an ASP.NET Core application that runs Elsa Studio. To run the container, simply run the following commands from your terminal:

```bash
docker pull elsaworkflows/elsa-studio-v3:latest
docker run -t -i -e ASPNETCORE_ENVIRONMENT='Development' -e HTTP_PORTS=8080 -e ELSASERVER__URL=http://localhost:13000/elsa/api -p 14000:8080 elsaworkflows/elsa-studio-v3:latest
```

{% hint style="warning" %}
**Requires Elsa Server**

Note that Elsa Studio needs to connect to an existing Elsa Server instance, which URL is configured via the `ELSASERVER__URL` environment variable passed to the container (on port `13000` in this example). To quickly start an Elsa Server instance, you can run the Elsa Server Docker image as outlined in the previous chapter.
{% endhint %}

When the container has started, open a web browser and navigate to [http://localhost:14000](http://localhost:14000/). On the login screen, enter the following credentials:

```shell-session
username: admin
password: password
```


# Docker Compose

Explore various Docker Compose configurations for setting up Elsa and related services like PostgreSQL and Traefik.


# Docker Quickstart

Get started quickly with Elsa Workflows using Docker Compose. This guide provides a fast path to evaluation with a complete setup including Elsa Server, Studio, and database persistence.

This quickstart guide helps you get Elsa Workflows up and running in minutes using Docker Compose. It's designed for evaluation, development, and learning purposes.

## Prerequisites

Before you begin, ensure you have:

* Docker Desktop (Windows/Mac) or Docker Engine (Linux) installed
* Docker Compose V2 or later
* At least 4GB of available RAM
* Ports 14000 and 5432 (if using PostgreSQL) available on your host machine

{% hint style="info" %}
**New to Docker?**

Visit the [Docker Prerequisites](/getting-started/prerequisites) page for installation instructions.
{% endhint %}

## Quick Start

### Option 1: SQLite (Simplest)

For the fastest start with minimal dependencies, use SQLite. This is perfect for evaluation and development.

Create a file named `docker-compose.yml` with the following content:

```yaml
services:
  elsa-server-and-studio:
    image: elsaworkflows/elsa-server-and-studio-v3:latest
    pull_policy: always
    environment:
      ASPNETCORE_ENVIRONMENT: Development
      HTTP_PORTS: 8080
      HTTP__BASEURL: http://localhost:14000
      DATABASEPROVIDER: Sqlite
      CONNECTIONSTRINGS__SQLITE: Data Source=/data/elsa.db;Cache=Shared
    ports:
      - "14000:8080"
    volumes:
      - elsa-data:/data
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

volumes:
  elsa-data:
    driver: local
```

Start the services:

```bash
docker compose up -d
```

Wait a few seconds for the services to start, then access Elsa Studio at [http://localhost:14000](http://localhost:14000/).

{% hint style="success" %}
**Default Credentials**

* Username: `admin`
* Password: `password`

Change these in production environments!
{% endhint %}

### Option 2: PostgreSQL (Production-Ready)

For production-like evaluation with a robust database, use PostgreSQL:

```yaml
services:
  postgres:
    image: postgres:16-alpine
    command: -c 'max_connections=200'
    environment:
      POSTGRES_USER: elsa
      POSTGRES_PASSWORD: elsa_password_change_me
      POSTGRES_DB: elsa
      POSTGRES_INITDB_ARGS: "--encoding=UTF8 --lc-collate=en_US.utf8 --lc-ctype=en_US.utf8"
    volumes:
      - postgres-data:/var/lib/postgresql/data
    # ports:
    #   - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U elsa"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  elsa-server-and-studio:
    image: elsaworkflows/elsa-server-and-studio-v3:latest
    pull_policy: always
    environment:
      ASPNETCORE_ENVIRONMENT: Development
      HTTP_PORTS: 8080
      HTTP__BASEURL: http://localhost:14000
      DATABASEPROVIDER: PostgreSql
      CONNECTIONSTRINGS__POSTGRESQL: "Server=postgres;Username=elsa;Database=elsa;Port=5432;Password=elsa_password_change_me;SSLMode=Prefer;MaxPoolSize=100;Timeout=60"
    ports:
      - "14000:8080"
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    restart: unless-stopped

volumes:
  postgres-data:
    driver: local
```

Start the services:

```bash
docker-compose up -d
```

Monitor the startup progress:

```bash
docker-compose logs -f
```

Once both services report healthy status, access Elsa Studio at [http://localhost:14000](http://localhost:14000/).

## Environment Variables Reference

### Core Configuration

| Variable                 | Description                                                 | Default | Required |
| ------------------------ | ----------------------------------------------------------- | ------- | -------- |
| `ASPNETCORE_ENVIRONMENT` | ASP.NET Core environment (Development, Staging, Production) | -       | Yes      |
| `HTTP_PORTS`             | Internal HTTP port for the container                        | `8080`  | Yes      |
| `HTTP__BASEURL`          | External base URL for the application                       | -       | Yes      |

### Database Configuration

| Variable                        | Description                  | Options                                      | Required            |
| ------------------------------- | ---------------------------- | -------------------------------------------- | ------------------- |
| `DATABASEPROVIDER`              | Database provider to use     | `Sqlite`, `PostgreSql`, `SqlServer`, `MySql` | Yes                 |
| `CONNECTIONSTRINGS__SQLITE`     | SQLite connection string     | `Data Source=/app/elsa.db;Cache=Shared`      | If using SQLite     |
| `CONNECTIONSTRINGS__POSTGRESQL` | PostgreSQL connection string | See example above                            | If using PostgreSQL |
| `CONNECTIONSTRINGS__SQLSERVER`  | SQL Server connection string | `Server=...`                                 | If using SQL Server |
| `CONNECTIONSTRINGS__MYSQL`      | MySQL connection string      | `Server=...`                                 | If using MySQL      |

### Optional Configuration

| Variable                       | Description                     | Default           |
| ------------------------------ | ------------------------------- | ----------------- |
| `ASPNETCORE_URLS`              | URLs the application listens on | `http://+:8080`   |
| `Logging__LogLevel__Default`   | Default log level               | `Information`     |
| `Logging__LogLevel__Microsoft` | Microsoft framework log level   | `Warning`         |
| `CORS__AllowedOrigins__0`      | CORS allowed origins            | `*` (Development) |

## Health Checks

Both configurations include health checks to ensure services are ready:

**Elsa Server Health Check**:

```yaml
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
  interval: 30s      # Check every 30 seconds
  timeout: 10s       # Timeout after 10 seconds
  retries: 3         # Retry 3 times before marking unhealthy
  start_period: 40s  # Grace period during startup
```

**PostgreSQL Health Check**:

```yaml
healthcheck:
  test: ["CMD-SHELL", "pg_isready -U elsa"]
  interval: 10s
  timeout: 5s
  retries: 5
```

Check health status:

```bash
docker-compose ps
```

## Common Operations

### View Logs

```bash
# All services
docker-compose logs -f

# Specific service
docker-compose logs -f elsa-server-and-studio

# Last 100 lines
docker-compose logs --tail=100
```

### Stop Services

```bash
# Stop without removing containers
docker-compose stop

# Stop and remove containers
docker-compose down

# Stop, remove containers, and delete volumes (⚠️ data loss!)
docker-compose down -v
```

### Restart Services

```bash
# Restart all services
docker-compose restart

# Restart specific service
docker-compose restart elsa-server-and-studio
```

### Update to Latest Version

```bash
# Pull latest images
docker-compose pull

# Recreate containers with new images
docker-compose up -d
```

## Troubleshooting

### Service Won't Start

**Symptom**: Container exits immediately after starting

**Solutions**:

1. Check logs for specific errors:

   ```bash
   docker-compose logs elsa-server-and-studio
   ```
2. Verify port availability:

   ```bash
   # Windows
   netstat -ano | findstr :14000

   # Linux/Mac
   lsof -i :14000
   ```
3. Ensure sufficient resources (RAM, disk space)

### Database Connection Failed

**Symptom**: Elsa can't connect to PostgreSQL

**Solutions**:

1. Verify PostgreSQL is healthy:

   ```bash
   docker-compose ps postgres
   ```
2. Check database logs:

   ```bash
   docker-compose logs postgres
   ```
3. Verify connection string matches PostgreSQL credentials
4. Wait for PostgreSQL to be fully initialized (can take 10-30 seconds on first start)

### Cannot Access Studio

**Symptom**: Browser can't reach <http://localhost:14000>

**Solutions**:

1. Verify service is running:

   ```bash
   docker-compose ps
   ```
2. Check if port is correctly mapped:

   ```bash
   docker-compose port elsa-server-and-studio 8080
   ```
3. Try accessing using container IP:

   ```bash
   docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' <container_name>
   ```
4. Check firewall settings blocking port 14000

### Performance Issues

**Symptom**: Slow response times or high memory usage

**Solutions**:

1. Allocate more resources in Docker Desktop settings (recommended: 4GB+ RAM)
2. Reduce PostgreSQL max\_connections if memory constrained:

   ```yaml
   command: -c 'max_connections=100'
   ```
3. Check container resource usage:

   ```bash
   docker stats
   ```

### Data Persistence Issues

**Symptom**: Workflows or data lost after restart

**Solutions**:

1. Verify volumes are correctly configured:

   ```bash
   docker volume ls
   ```
2. Check volume mounts:

   ```bash
   docker inspect <container_name>
   ```
3. Don't use `docker-compose down -v` unless you want to delete data

### Authentication Problems

**Symptom**: Can't log in with default credentials

**Solutions**:

1. Ensure you're using:
   * Username: `admin`
   * Password: `password`
2. Clear browser cache and cookies
3. Try incognito/private browsing mode
4. Check Elsa logs for authentication errors

## Production Considerations

{% hint style="warning" %}
**This configuration is designed for evaluation and development. For production deployments, consider the following:**
{% endhint %}

### Security

1. **Change Default Credentials**: Never use default admin credentials in production
2. **Use Strong Passwords**: For both Elsa and database users
3. **Enable HTTPS**: Configure TLS/SSL certificates
4. **Restrict Database Access**: Don't expose database ports publicly
5. **Use Secrets Management**: Store sensitive data in Docker secrets or environment-specific vaults
6. **Configure CORS Properly**: Restrict allowed origins to known domains

Example with secrets:

```yaml
services:
  postgres:
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
    secrets:
      - postgres_password

secrets:
  postgres_password:
    file: ./secrets/postgres_password.txt
```

### Scalability

1. **Use External Database**: Host database outside Docker for better performance and reliability
2. **Load Balancing**: Deploy multiple Elsa instances behind a load balancer
3. **Connection Pooling**: Adjust MaxPoolSize based on load
4. **Resource Limits**: Configure CPU and memory limits:

   ```yaml
   deploy:
     resources:
       limits:
         cpus: '2'
         memory: 2G
       reservations:
         cpus: '1'
         memory: 1G
   ```

### Reliability

1. **Regular Backups**: Implement automated database backups

   > **Note:** The following command uses Unix/Linux shell syntax. For Windows users, see the PowerShell alternative below.

   ```bash
   # PostgreSQL backup example (Unix/Linux/macOS)
   docker-compose exec postgres pg_dump -U elsa elsa > backup_$(date +%Y%m%d).sql

   ```
2. **Monitoring**: Integrate with monitoring solutions (Prometheus, Grafana, etc.)
3. **Logging**: Configure centralized logging (ELK stack, Loki, etc.)
4. **Health Checks**: Keep health checks enabled and configure orchestrator accordingly
5. **Update Strategy**: Plan for zero-downtime updates

### Database Performance

1. **Optimize PostgreSQL Configuration**:

   ```yaml
   command: >
     -c 'max_connections=200'
     -c 'shared_buffers=256MB'
     -c 'effective_cache_size=1GB'
     -c 'maintenance_work_mem=64MB'
     -c 'checkpoint_completion_target=0.9'
     -c 'wal_buffers=16MB'
     -c 'default_statistics_target=100'
     -c 'random_page_cost=1.1'
   ```
2. **Regular Maintenance**: Schedule VACUUM and ANALYZE operations
3. **Monitor Query Performance**: Use pg\_stat\_statements extension

### Data Management

1. **Volume Backups**: Regularly backup Docker volumes

   ```bash
   docker run --rm -v postgres-data:/data -v $(pwd):/backup alpine tar czf /backup/postgres-data-backup.tar.gz -C /data .

   ```
2. **Retention Policies**: Configure workflow and log retention
3. **Archival Strategy**: Move old workflows to cold storage

## Next Steps

Now that you have Elsa running:

1. **Explore the Studio**: Navigate to [http://localhost:14000](http://localhost:14000/) and explore the interface
2. **Create Your First Workflow**: Follow the [Hello World](/getting-started/hello-world) tutorial
3. **Learn Key Concepts**: Read about [Workflows, Activities, and Triggers](/getting-started/concepts)
4. **Try HTTP Workflows**: Build REST APIs using [HTTP Workflows](/guides/http-workflows)
5. **Extend Elsa**: Create [Custom Activities](/extensibility/custom-activities)

## Alternative Configurations

For other deployment scenarios, see:

* [Elsa Server + Studio (Separate Images)](/getting-started/containers/docker-compose/elsa-server-+-studio) - Run as separate containers
* [Persistent Database](/getting-started/containers/docker-compose/persistent-database) - More database configuration examples
* [Database Configuration](/getting-started/database-configuration) - Complete database setup guide
* [Persistence Guide](/guides/persistence) - Comprehensive persistence provider documentation
* [Traefik Integration](/getting-started/containers/docker-compose/traefik) - Reverse proxy setup
* [Distributed Hosting](/hosting/distributed-hosting) - Multi-instance deployments

## Support

If you encounter issues not covered in this guide:

* Check the [Elsa Workflows GitHub Discussions](https://github.com/elsa-workflows/elsa-core/discussions)
* Review [GitHub Issues](https://github.com/elsa-workflows/elsa-core/issues)
* Join the community on Discord or Slack

## Version Information

This guide is written for:

* Elsa Workflows v3.7.0
* Docker Compose V2
* PostgreSQL 16
* SQLite 3

Always check the [official releases](https://github.com/elsa-workflows/elsa-core/releases) for the latest version information.


# Elsa Server + Studio

Using Docker Compose, you can quickly set up and run both Elsa Server and Elsa Studio. This guide walks you through creating a docker-compose.yml file to deploy these services.

### Docker Compose File Structure <a href="#compose-file-structure" id="compose-file-structure"></a>

Below is the full content of a `docker-compose.yml` file for deploying Elsa Server and Elsa Studio with minimal configuration:

```yaml
services:

    # Elsa Server.
    elsa-server:
        image: elsaworkflows/elsa-server-v3:latest
        pull_policy: always
        environment:
            ASPNETCORE_ENVIRONMENT: Development
            HTTP_PORTS: 8080
            HTTP__BASEURL: http://localhost:12000
        ports:
            - "12000:8080"

    # Elsa Studio connected to Elsa Server.
    elsa-studio:
        image: elsaworkflows/elsa-studio-v3:latest
        pull_policy: always
        environment:
            ASPNETCORE_ENVIRONMENT: Development
            HTTP_PORTS: 8080
            HTTP__BASEURL: http://localhost:13000
            ELSASERVER__URL: http://localhost:12000/elsa/api
        ports:
            - "13000:8080"
        depends_on:
            - elsa-server
```

Save this file as `docker-compose.yml` in your working directory.

### Running the Docker Compose File <a href="#running-compose" id="running-compose"></a>

To start the services defined in the `docker-compose.yml` file, use the following command in your terminal:

```
docker-compose up
```

This command will:

* Pull the necessary Docker images (elsaworkflows/elsa-server-v3 and elsaworkflows/elsa-studio-v3).
* Start both services (Elsa Server and Elsa Studio).

Once the services are running, you can access them at the following URLs:

* Elsa Server: [http://localhost:12000](http://localhost:12000/)
* Elsa Studio: [http://localhost:13000](http://localhost:13000/)

### Service Configuration Details <a href="#service-configuration" id="service-configuration"></a>

Here is a quick overview of the services defined in the Docker Compose file:

#### Elsa Server <a href="#elsa-server-details" id="elsa-server-details"></a>

Elsa Server is configured to run on `http://localhost:12000`. Key environment variables include:

* `ASPNETCORE_ENVIRONMENT`: Specifies the environment (e.g., `Development`).
* `HTTP_PORTS`: Specifies the HTTP port within the container.
* `HTTP__BASEURL`: Sets the base URL for the server.

#### Elsa Studio <a href="#elsa-studio-details" id="elsa-studio-details"></a>

Elsa Studio is configured to run on `http://localhost:13000`. It connects to Elsa Server at `http://localhost:12000/elsa/api`. Key environment variables include:

* `ASPNETCORE_ENVIRONMENT`: Specifies the environment (e.g., `Development`).
* `HTTP_PORTS`: Specifies the HTTP port within the container.
* `HTTP__BASEURL`: Sets the base URL for the Studio.
* `ELSASERVER__URL`: Configures the URL of the connected Elsa Server.

{% hint style="info" %}
**Network Configuration**

Ensure the ports specified in the docker-compose.yml file (e.g., `12000:8080` and `13000:8080`) are not already in use on your system. If they are, adjust the port mappings to avoid conflicts.
{% endhint %}


# Elsa Server + Studio - Single Image

This guide demonstrates how to set up Elsa Server and Studio using Docker Compose, enabling you to run both components from a single Docker image.

### Docker Compose Configuration <a href="#docker-compose-configuration" id="docker-compose-configuration"></a>

Below is an example Docker Compose configuration that sets up the Elsa Server + Studio application:

```yaml
services:

    # Elsa Studio and Server from a single image.
    elsa-server-and-studio:
        image: elsaworkflows/elsa-server-and-studio-v3:latest
        pull_policy: always
        environment:
            ASPNETCORE_ENVIRONMENT: Development
            HTTP_PORTS: 8080
            HTTP__BASEURL: http://localhost:14000
        ports:
            - "14000:8080"
```

### Steps to Set Up <a href="#steps-to-set-up" id="steps-to-set-up"></a>

* Create a `docker-compose.yml` file in your project directory with the above configuration.
* Ensure that Docker and Docker Compose are installed on your machine. Refer to the [prerequisites documentation](https://elsa-workflows.github.io/elsa-documentation/prerequisites.html#docker) for installation guidance.
* Open a terminal in the directory containing the `docker-compose.yml` file.
* Run the following command to start the container:

  ```bash
  docker-compose up
  ```

### Accessing Elsa <a href="#accessing-elsa" id="accessing-elsa"></a>

Once the container is running, you can access Elsa Studio in your browser at: [http://localhost:14000](http://localhost:14000/).

Use the default admin credentials to log in.

```
username: admin
password: password
```


# Persistent Database

This topic provides steps to set up Elsa Server and Studio with a PostgreSQL database using Docker Compose. PostgreSQL is used as an example - other database engines are supported as well, including MySql and SQL Server.

### Docker Compose Configuration <a href="#docker-compose-setup" id="docker-compose-setup"></a>

Below is the Docker Compose file used to set up Elsa with PostgreSQL:

```yaml
services:

    postgres:
        image: postgres:latest
        command: -c 'max_connections=2000'
        environment:
            POSTGRES_USER: elsa
            POSTGRES_PASSWORD: elsa
            POSTGRES_DB: elsa
        volumes:
            - postgres-data:/var/lib/postgresql/data
        ports:
            - "5432:5432"

    elsa-server-and-studio:
        image: elsaworkflows/elsa-server-and-studio-v3:latest
        pull_policy: always
        environment:
            ASPNETCORE_ENVIRONMENT: Development
            HTTP_PORTS: 8080
            HTTP__BASEURL: http://localhost:14000
            DATABASEPROVIDER: PostgreSql
            CONNECTIONSTRINGS__POSTGRESQL: Server=postgres;Username=elsa;Database=elsa;Port=5432;Password=elsa;SSLMode=Prefer;MaxPoolSize=2000;Timeout=60
        ports:
            - "14000:8080"
        depends_on:
            - postgres

volumes:
    postgres-data:
```

### Configuration Details <a href="#configuration-details" id="configuration-details"></a>

The Docker Compose file defines two services:

* PostgreSQL Service: A PostgreSQL database container configured with the following settings:
  * User: `elsa`
  * Password: `elsa`
  * Database: `elsa`
  * Max Connections: `2000`
* Elsa Server + Studio: A container running Elsa Server and Studio, configured to use PostgreSQL as the database provider.
  * Environment Variables: Defines `DATABASEPROVIDER` as `PostgreSql` and the PostgreSQL connection string in `CONNECTIONSTRINGS__POSTGRESQL`.
  * Ports: Maps port `14000` on the host to `8080` in the container.

### Supported Database Providers <a href="#supported-db-providers" id="supported-db-providers"></a>

Elsa supports multiple database providers, which can be configured using the `DATABASEPROVIDER` environment variable:

* `SqlServer`
* `Sqlite` (default)
* `MySql`
* `PostgreSql`

In this setup, `PostgreSql` is used as the database provider.

### Running the Services <a href="#running-services" id="running-services"></a>

To run the services defined in the Docker Compose file, use the following command:

```bash
docker-compose up
```

Once the services are running, you can access Elsa Studio by navigating to [http://localhost:14000](http://localhost:14000/).

## See Also

For more detailed information about database configuration and persistence providers:

* [Database Configuration](/getting-started/database-configuration) - Complete database setup guide
* [Persistence Guide](/guides/persistence) - Comprehensive persistence provider overview
* [SQL Server Guide](/guides/persistence/sql-server) - SQL Server-specific configuration (if using SQL Server instead of PostgreSQL)


# Traefik

This guide walks you through setting up and running Elsa Server and Studio using a Docker Compose file. The setup includes PostgreSQL as the database, Traefik as a reverse proxy, and Elsa workflows.

### Docker Compose Configuration <a href="#docker-compose-config" id="docker-compose-config"></a>

The following `docker-compose.yml` file defines services for:

* PostgreSQL database for data persistence.
* Elsa Server and Studio, configured to use PostgreSQL.
* Traefik reverse proxy for routing requests to the appropriate service.

```yaml
services:

    # PostgreSQL database.
    postgres:
        image: postgres:latest
        command: -c 'max_connections=2000'
        environment:
            POSTGRES_USER: elsa
            POSTGRES_PASSWORD: elsa
            POSTGRES_DB: elsa
        volumes:
            - postgres-data:/var/lib/postgresql/data
        ports:
            - "5432:5432"
        networks:
            - elsa-network

    # Elsa Server and Studio behind Traefik and configured with PostgreSQL.
    elsa-server-and-studio:
        image: elsaworkflows/elsa-server-and-studio-v3:latest
        pull_policy: always
        environment:
            ASPNETCORE_ENVIRONMENT: Development
            HTTP_PORTS: 8080
            HTTP__BASEURL: http://elsa.localhost:1280
            DATABASEPROVIDER: PostgreSql
            CONNECTIONSTRINGS__POSTGRESQL: Host=postgres;Port=5432;Database=elsa;Username=elsa;Password=elsa
        labels:
            - "traefik.enable=true"
            - "traefik.http.routers.elsa.rule=Host(`elsa.localhost`)"
            - "traefik.http.services.elsa.loadbalancer.server.port=8080"
        networks:
            - elsa-network

    # Traefik reverse proxy.
    traefik:
        image: traefik:2.7.2
        command:
            - "--api.insecure=true" # Enables the Traefik dashboard
            - "--providers.docker=true" # Enables Docker as the configuration source
            - "--entrypoints.web.address=:80" # Sets up the HTTP entry point on port 80
        ports:
            - "1280:80" # Expose HTTP port. Access Elsa Studio at: http://elsa.localhost:1280/
            - "8080:8080" # Expose Traefik dashboard
        volumes:
            - "/var/run/docker.sock:/var/run/docker.sock" # Allows Traefik to communicate with the Docker daemon
        networks:
            - elsa-network
        depends_on:
            - elsa-server-and-studio

networks:
    elsa-network:
        driver: bridge

volumes:
    postgres-data:
```

### Setup Instructions <a href="#setup-instructions" id="setup-instructions"></a>

Follow these steps to set up and run the Docker Compose configuration:

* Ensure you have Docker and Docker Compose installed on your machine. Refer to the [prerequisites](https://elsa-workflows.github.io/elsa-documentation/prerequisites.html#docker) if necessary.
* Create a file named `docker-compose.yml` and paste the provided configuration into it.
* Run the following command in the directory containing the `docker-compose.yml` file to start the services:

  ```
  docker-compose up
  ```
* Edit your `/etc/hosts` file (on Linux/Mac) or `C:\Windows\System32\drivers\etc\hosts` (on Windows) to include the following entry for mapping `elsa.localhost` to `127.0.0.1`:

  ```
  127.0.0.1 elsa.localhost
  ```
* Once the services are running:
  * Access Elsa Studio at [http://elsa.localhost:1280](http://elsa.localhost:1280/).
  * Open the Traefik dashboard at [http://localhost:8080](http://localhost:8080/).

### Environment Configuration <a href="#env-configuration" id="env-configuration"></a>

The environment variables and settings used in this Docker Compose file:

* PostgreSQL: The database user, password, and name are configured as `elsa`.
* Elsa Server and Studio: Configured to use PostgreSQL as the database provider.
* Traefik: Acts as a reverse proxy with routing rules for `elsa.localhost`.

### Troubleshooting <a href="#troubleshooting" id="troubleshooting"></a>

If you encounter issues, check the following:

* Ensure Docker and Docker Compose are correctly installed and running.
* Verify the `/etc/hosts` file includes an entry for `elsa.localhost` mapping to `127.0.0.1`.
* Inspect logs for each service using `docker-compose logs [service-name]`.


# Elsa Server

In this topic, we'll create an ASP.NET Core application that acts as a workflow server.

An Elsa Server is an ASP.NET Core web application that lets you manage workflows using a REST API and execute them. You can store your workflows in various places like databases, file systems, or even cloud storage.

## Setup <a href="#setup" id="setup"></a>

The following is a step-by-step guide to setting up a new ASP.NET Core Web Application that serves as an Elsa Server.

1. **Create a new ASP.NET project**

   Open your command line tool and run these commands:

   ```bash
   dotnet new web -n "ElsaServer"
   ```
2. **CD into the project's directory**

   Run the following command to go into the project's directory.

   ```bash
   cd ElsaServer
   ```
3. **Add Packages**

   Add some commonly used Elsa packages.

   ```bash
   dotnet add package Elsa
   dotnet add package Elsa.Persistence.EFCore
   dotnet add package Elsa.Persistence.EFCore.Sqlite
   dotnet add package Elsa.Http
   dotnet add package Elsa.Identity
   dotnet add package Elsa.Scheduling
   dotnet add package Elsa.Workflows.Api
   dotnet add package Elsa.Expressions.CSharp
   dotnet add package Elsa.Http
   dotnet add package Elsa.Expressions.JavaScript
   dotnet add package Elsa.Expressions.Liquid
   ```
4. We need to add some code to make our server work. Open the `Program.cs` file in your project and replace its contents with the code provided below. This code does a lot of things like setting up database connections, enabling user authentication, and preparing the server to handle workflows.

   \
   **Program.cs**

   ```csharp
   using Elsa.Persistence.EFCore.Extensions;
   using Elsa.Persistence.EFCore.Modules.Management;
   using Elsa.Persistence.EFCore.Modules.Runtime;
   using Elsa.Extensions;

   var builder = WebApplication.CreateBuilder(args);
   builder.Services.AddElsa(elsa =>
   {
       // Configure Management layer to use EF Core.
       elsa.UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => ef.UseSqlite()));

       // Configure Runtime layer to use EF Core.
       elsa.UseWorkflowRuntime(runtime => runtime.UseEntityFrameworkCore(ef => ef.UseSqlite()));

       // Default Identity features for authentication/authorization.
       elsa.UseIdentity(identity =>
       {
           identity.TokenOptions = options => options.SigningKey = "sufficiently-large-secret-signing-key"; // This key needs to be at least 256 bits long.
           identity.UseAdminUserProvider();
       });

       // Configure ASP.NET authentication/authorization.
       elsa.UseDefaultAuthentication(auth => auth.UseAdminApiKey());

       // Expose Elsa API endpoints.
       elsa.UseWorkflowsApi();

       // Setup a SignalR hub for real-time updates from the server.
       elsa.UseRealTimeWorkflows();

       // Enable C# workflow expressions
       elsa.UseCSharp();
       
       // Enable JavaScript workflow expressions
       elsa.UseJavaScript(options => options.AllowClrAccess = true);

       // Enable HTTP activities.
       elsa.UseHttp(options => options.ConfigureHttpOptions = httpOptions => httpOptions.BaseUrl = new("https://localhost:5001"));

       // Use timer activities.
       elsa.UseScheduling();

       // Register custom activities from the application, if any.
       elsa.AddActivitiesFrom<Program>();

       // Register custom workflows from the application, if any.
       elsa.AddWorkflowsFrom<Program>();
   });

   // Configure CORS to allow designer app hosted on a different origin to invoke the APIs.
   builder.Services.AddCors(cors => cors
       .AddDefaultPolicy(policy => policy
           .AllowAnyOrigin() // For demo purposes only. Use a specific origin instead.
           .AllowAnyHeader()
           .AllowAnyMethod()
           .WithExposedHeaders("x-elsa-workflow-instance-id"))); // Required for Elsa Studio in order to support running workflows from the designer. Alternatively, you can use the `*` wildcard to expose all headers.

   // Add Health Checks.
   builder.Services.AddHealthChecks();

   // Build the web application.
   var app = builder.Build();

   // Configure web application's middleware pipeline.
   app.UseCors();
   app.UseRouting(); // Required for SignalR.
   app.UseAuthentication();
   app.UseAuthorization();
   app.UseWorkflowsApi(); // Use Elsa API endpoints.
   app.UseWorkflows(); // Use Elsa middleware to handle HTTP requests mapped to HTTP Endpoint activities.
   app.UseWorkflowsSignalRHubs(); // Optional SignalR integration. Elsa Studio uses SignalR to receive real-time updates from the server. 

   app.Run();
   ```

## Launch the Application <a href="#run-application" id="run-application"></a>

To see the application in action, execute the following command:

```bash
dotnet run --urls "https://localhost:5001"
```

## Source Code <a href="#source-code" id="source-code"></a>

The source code for this chapter can be found [here](https://github.com/elsa-workflows/elsa-guides/tree/main/src/installation/elsa-server)


# Elsa Studio

In this topic, we will create a separate ASP.NET Blazor Webassembly app and turn it into an Elsa Studio that connects to an Elsa Server.

Elsa Studio is a Blazor application that let's you manage workflows through a UI. The application is essentially a SPA that connects to an Elsa Server as its back-end.

## Setup <a href="#setup" id="setup"></a>

To setup Elsa Studio, we'll go through the following steps:

{% hint style="warning" %}
**Deprecation warning**

The `blazorwasm-empty` template is [dicontinued since .NET 8.0](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-new-sdk-templates).\
If you are using .NET 8.0+, you can just use `blazorwasm` instead of `blazorwasm-empty`.
{% endhint %}

1. **Create a New Blazor Webassembly App**

   Execute the following command in the terminal:

   ```bash
   dotnet new blazorwasm -n "ElsaStudioBlazorWasm"
   ```

   \\
2. **Add Elsa Studio Packages**

   Navigate to the root directory of your project and integrate the following Elsa Studio packages:

   ```
   cd ElsaStudioBlazorWasm
   dotnet add package Elsa.Studio
   dotnet add package Elsa.Studio.Core.BlazorWasm
   dotnet add package Elsa.Studio.Authentication.ElsaIdentity.BlazorWasm
   dotnet add package Elsa.Studio.Authentication.ElsaIdentity.UI
   dotnet add package Elsa.Studio.Authentication.OpenIdConnect.BlazorWasm
   dotnet add package Elsa.Studio.Localization.BlazorWasm
   dotnet add package Elsa.Api.Client
   ```
3. **Modify Program.cs**

   Open the `Program.cs` file and replace its existing content with the code provided below:

   **Program.cs**

   ```csharp
   using Elsa.Studio.Authentication.ElsaIdentity.BlazorWasm.Extensions;
   using Elsa.Studio.Authentication.ElsaIdentity.HttpMessageHandlers;
   using Elsa.Studio.Authentication.ElsaIdentity.UI.Extensions;
   using Elsa.Studio.Authentication.OpenIdConnect.BlazorWasm.Extensions;
   using Elsa.Studio.Authentication.OpenIdConnect.HttpMessageHandlers;
   using Elsa.Studio.Contracts;
   using Elsa.Studio.Core.BlazorWasm.Extensions;
   using Elsa.Studio.Dashboard.Extensions;
   using Elsa.Studio.Extensions;
   using Elsa.Studio.Localization.BlazorWasm.Extensions;
   using Elsa.Studio.Localization.Models;
   using Elsa.Studio.Models;
   using Elsa.Studio.Shell;
   using Elsa.Studio.Shell.Extensions;
   using Elsa.Studio.Workflows.Designer.Extensions;
   using Elsa.Studio.Workflows.Extensions;
   using Microsoft.AspNetCore.Components.Web;
   using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

   // Build the host.
   var builder = WebAssemblyHostBuilder.CreateDefault(args);
   var configuration = builder.Configuration;

   // Register root components.
   builder.RootComponents.Add<App>("#app");
   builder.RootComponents.Add<HeadOutlet>("head::after");
   builder.RootComponents.RegisterCustomElsaStudioElements();

   // Choose authentication provider.
   // Supported values: "OpenIdConnect" or "ElsaIdentity".
   var authProvider = configuration["Authentication:Provider"];
   if (string.IsNullOrWhiteSpace(authProvider))
       authProvider = "ElsaIdentity";

   Type authenticationHandler;

   if (authProvider.Equals("ElsaIdentity", StringComparison.OrdinalIgnoreCase))
   {
       // Elsa Identity (username/password against Elsa backend) + login UI at /login.
       builder.Services.AddElsaIdentity();
       builder.Services.AddElsaIdentityUI();
       authenticationHandler = typeof(ElsaIdentityAuthenticatingApiHttpMessageHandler);
   }
   else if (authProvider.Equals("OpenIdConnect", StringComparison.OrdinalIgnoreCase))
   {
       // OpenID Connect.
       builder.Services.AddOpenIdConnectAuth(options =>
       {
           configuration.GetSection("Authentication:OpenIdConnect").Bind(options);
       });
       authenticationHandler = typeof(OidcAuthenticatingApiHttpMessageHandler);
   }
   else
   {
       throw new InvalidOperationException($"Unsupported Authentication:Provider value '{authProvider}'. Supported values are 'OpenIdConnect' and 'ElsaIdentity'.");
   }

   // Register shell services and modules.
   var backendApiConfig = new BackendApiConfig
   {
       ConfigureBackendOptions = options => builder.Configuration.GetSection("Backend").Bind(options),
       ConfigureHttpClientBuilder = options => options.AuthenticationHandler = authenticationHandler
   };

   var localizationConfig = new LocalizationConfig
   {
       ConfigureLocalizationOptions = options => configuration.GetSection("Localization").Bind(options)
   };

   builder.Services.AddCore();
   builder.Services.AddShell();
   builder.Services.AddRemoteBackend(backendApiConfig);
   builder.Services.AddDashboardModule();
   builder.Services.AddWorkflowsModule();
   builder.Services.AddLocalizationModule(localizationConfig);

   // Build the application.
   var app = builder.Build();

   await app.UseElsaLocalization();

   // Run each startup task.
   var startupTaskRunner = app.Services.GetRequiredService<IStartupTaskRunner>();
   await startupTaskRunner.RunStartupTasksAsync();

   // Run the application.
   await app.RunAsync();
   ```
4. **Remove Unnecessary Files**

   For a cleaner project structure, delete the following directories and files:

   * wwwroot/css
   * Pages
   * App.razor
   * MainLayout.razor
   * MainLayout.razor.css
   * \_Imports.razor
5. **Generate appsettings.json**

   Within the `wwwroot` directory, create a new `appsettings.json` file and populate it with the following content:

   wwwroot/appsettings.json

   ```json
   {
       "Backend": {
           "Url": "https://localhost:5001/elsa/api"
       },
       "Authentication": {
           "Provider": "OpenIdConnect",
           "OpenIdConnect": {
               "Authority": "https://login.microsoftonline.com/{tenant-id}/v2.0",
               "ClientId": "{client-id}",
               "AuthenticationScopes": [
                   "openid",
                   "profile",
                   "offline_access"
               ],
               "BackendApiScopes": [
                   "api://{backend-api-client-id}/elsa-server-api"
               ]
           }
       },
       "Localization": {
           "DefaultCulture": "en-US",
           "SupportedCultures": [
               "en-US"
           ]
       }
   }
   ```

   `AuthenticationScopes` are used during Studio sign-in. `BackendApiScopes` are used when Studio requests bearer tokens for Elsa Server API calls.

   Because this example is a Blazor WebAssembly host, register `{studio-url}/authentication/login-callback` as the redirect URI and `{studio-url}/authentication/logout-callback` as the logout callback URI. Studio initiates logout at `{studio-url}/authentication/logout`.
6. **Update index.html**

   To conclude the setup, open the `index.html` file and replace its content with the code showcased below:

   **wwwroot/index.html**

   ```html
   <!DOCTYPE html>
   <html>

   <head>
       <meta charset="utf-8"/>
       <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
       <title>Elsa Studio</title>
       <base href="/"/>
       <link rel="apple-touch-icon" sizes="180x180" href="_content/Elsa.Studio.Shell/apple-touch-icon.png">
       <link rel="icon" type="image/png" sizes="32x32" href="_content/Elsa.Studio.Shell/favicon-32x32.png">
       <link rel="icon" type="image/png" sizes="16x16" href="_content/Elsa.Studio.Shell/favicon-16x16.png">
       <link rel="manifest" href="_content/Elsa.Studio.Shell/site.webmanifest">
       <link rel="preconnect" href="https://fonts.googleapis.com">
       <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
       <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
       <link href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;500;700&display=swap" rel="stylesheet">
       <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
       <link href="https://fonts.googleapis.com/css2?family=Grandstander:wght@100&display=swap" rel="stylesheet">
       <link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
       <link href="_content/CodeBeam.MudBlazor.Extensions/MudExtensions.min.css" rel="stylesheet" />
       <link href="_content/Radzen.Blazor/css/material-base.css" rel="stylesheet" >
       <link href="_content/Elsa.Studio.Shell/css/shell.css" rel="stylesheet">
       <link href="ElsaStudioBlazorWasm.styles.css" rel="stylesheet">
   </head>

   <body>
   <div id="app">
       <div class="loading-splash mud-container mud-container-maxwidth-false">
           <h5 class="mud-typography mud-typography-h5 mud-primary-text my-6">Loading...</h5>
       </div>
   </div>

   <div id="blazor-error-ui">
       An unhandled error has occurred.
       <a href="" class="reload">Reload</a>
       <a class="dismiss">🗙</a>
   </div>
   <script src="_content/BlazorMonaco/jsInterop.js"></script>
   <script src="_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js"></script>
   <script src="_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js"></script>
   <script src="_content/MudBlazor/MudBlazor.min.js"></script>
   <script src="_content/CodeBeam.MudBlazor.Extensions/MudExtensions.min.js"></script>
   <script src="_content/Radzen.Blazor/Radzen.Blazor.js"></script>
   <script src="_framework/blazor.webassembly.js"></script>
   </body>

   </html>
   ```

## Launch the Application <a href="#run-application" id="run-application"></a>

To see your application in action, execute the following command:

```bash
dotnet run --urls https://localhost:6001
```

Your application is now accessible at [https://localhost:6001](https://localhost:6001/).

By default, you can log in using:

```
username: admin
password: password
```

## Source Code <a href="#source-code" id="source-code"></a>

The source code for this chapter can be found [here](https://github.com/elsa-workflows/elsa-guides/tree/main/src/installation/elsa-studio/ElsaStudioBlazorWasm)


# Elsa Server + Studio (WASM)

In this topic, we will create an ASP.NET Core application that acts as both an Elsa Server and an Elsa Studio.

Instead of running Elsa Server and Elsa Studio as separate ASP.NET Core applications, you can also setup an ASP.NET Core application that hosts both the workflow server and the UI. The UI will still make HTTP calls to the backend as if they were hosted separately, but the difference is that they are now served from the same application and therefore deployable as a single unit.

For Elsa Studio, we will setup the Blazor parts using Blazor WebAssembly, which static files will be served from the ASP.NET Core host application.

## Create Solution <a href="#create-solution" id="create-solution"></a>

In this chapter, we will scaffold a new solution and two projects:

* The Host
* The Client

The host will host both Elsa Server and the Blazor WebAssembly application representing Elsa Studio.

Run the following commands to create a solution with two projects:

```bash
# Create a new solution
dotnet new sln -n ElsaServerAndStudio

# Create the host project
dotnet new web -n "ElsaServer"

# Add the host project to the solution
dotnet sln add ElsaServer/ElsaServer.csproj

# Create the client project
dotnet new blazorwasm -n "ElsaStudio"

# Add the client project to the solution
dotnet sln add ElsaStudio/ElsaStudio.csproj

# Navigate to the directory where the host project is located
cd ElsaServer

# Add a reference to the client project
dotnet add reference ../ElsaStudio/ElsaStudio.csproj
```

## Setup Host <a href="#setup-host" id="setup-host"></a>

In this chapter, we will setup the host, which will host both the Elsa Server engine as well as the webassembly files for serving the Elsa Studio client assets to the browser.

1. **Add Packages**

   Add the following packages:

   ```bash
   dotnet add package Elsa
   dotnet add package Elsa.Persistence.EFCore
   dotnet add package Elsa.Persistence.EFCore.Sqlite
   dotnet add package Elsa.Http
   dotnet add package Elsa.Identity
   dotnet add package Elsa.Scheduling
   dotnet add package Elsa.Workflows.Api
   dotnet add package Elsa.Expressions.CSharp
   dotnet add package Elsa.Expressions.JavaScript
   dotnet add package Elsa.Expressions.Liquid
   dotnet add package Microsoft.AspNetCore.Components.WebAssembly.Server
   ```
2. **Update Program.cs**

   Open the *Program.cs* file in your project and replace its contents with the code provided below. This code does a lot of things like setting up database connections, enabling user authentication, and preparing the server to handle workflows.

   **Program.cs**

   ```csharp
   using Elsa.Persistence.EFCore.Extensions;
   using Elsa.Persistence.EFCore.Modules.Management;
   using Elsa.Persistence.EFCore.Modules.Runtime;
   using Elsa.Extensions;
   using Microsoft.AspNetCore.Mvc;

   var builder = WebApplication.CreateBuilder(args);
   builder.WebHost.UseStaticWebAssets();

   var services = builder.Services;
   var configuration = builder.Configuration;

   services
       .AddElsa(elsa => elsa
           .UseIdentity(identity =>
           {
               identity.TokenOptions = options => options.SigningKey = "large-signing-key-for-signing-JWT-tokens";
               identity.UseAdminUserProvider();
           })
           .UseDefaultAuthentication()
           .UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => ef.UseSqlite()))
           .UseWorkflowRuntime(runtime => runtime.UseEntityFrameworkCore(ef => ef.UseSqlite()))
           .UseScheduling()
           .UseJavaScript()
           .UseLiquid()
           .UseCSharp()
           .UseHttp(http => http.ConfigureHttpOptions = options => configuration.GetSection("Http").Bind(options))
           .UseWorkflowsApi()
           .AddActivitiesFrom<Program>()
           .AddWorkflowsFrom<Program>()
       );

   services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().WithExposedHeaders("*")));
   services.AddRazorPages(options => options.Conventions.ConfigureFilter(new IgnoreAntiforgeryTokenAttribute()));

   var app = builder.Build();

   if (!app.Environment.IsDevelopment())
   {
       app.UseExceptionHandler("/Error");
       app.UseHsts();
   }

   app.UseHttpsRedirection();
   app.MapStaticAssets();
   app.UseRouting();
   app.UseCors();
   app.UseStaticFiles();
   app.UseAuthentication();
   app.UseAuthorization();
   app.UseWorkflowsApi();
   app.UseWorkflows();
   app.MapFallbackToPage("/_Host");
   app.Run();
   ```
3. **Update appsettings.json**

   Add the following configuration section to `appsettings.json` or `appsettings.Development.json` with the following content:

   ```json
   {
       "Http": {
           "BaseUrl": "https://localhost:5001",
           "BasePath": "/api/workflows"
       }
   }
   ```
4. **Create \_Host.cshtml**

   To conclude the setup, create new folder called `Pages` and add a new file called `_Host.cshtml` and copy in the code showcased below:

   **Pages/\_Host.cshtml**

   ```cshtml
   @page "/"
   @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
   @{
       var baseUrl = $"{Request.Scheme}://{Request.Host}";
       var apiUrl = baseUrl + Url.Content("~/elsa/api");
       var basePath = "";
   }

   <!DOCTYPE html>
   <html>

   <head>
       <meta charset="utf-8"/>
       <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
       <title>Elsa Studio 3.0</title>
       <base href="/"/>
       <link rel="apple-touch-icon" sizes="180x180" href="@basePath/_content/Elsa.Studio.Shell/apple-touch-icon.png">
       <link rel="icon" type="image/png" sizes="32x32" href="@basePath/_content/Elsa.Studio.Shell/favicon-32x32.png">
       <link rel="icon" type="image/png" sizes="16x16" href="@basePath/_content/Elsa.Studio.Shell/favicon-16x16.png">
       <link rel="manifest" href="@basePath/_content/Elsa.Studio.Shell/site.webmanifest">
       <link rel="preconnect" href="https://fonts.googleapis.com">
       <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
       <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet"/>
       <link href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;500;700&display=swap" rel="stylesheet">
       <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
       <link href="https://fonts.googleapis.com/css2?family=Grandstander:wght@100&display=swap" rel="stylesheet">
       <link href="@basePath/_content/MudBlazor/MudBlazor.min.css" rel="stylesheet"/>
       <link href="@basePath/_content/CodeBeam.MudBlazor.Extensions/MudExtensions.min.css" rel="stylesheet"/>
       <link href="@basePath/_content/Radzen.Blazor/css/material-base.css" rel="stylesheet">
       <link href="@basePath/_content/Elsa.Studio.Shell/css/shell.css" rel="stylesheet">
       <link href="ElsaStudio.styles.css" rel="stylesheet">
   </head>

   <body>
   <div id="app">
       <div class="loading-splash mud-container mud-container-maxwidth-false">
           <h5 class="mud-typography mud-typography-h5 mud-primary-text my-6">Loading...</h5>
       </div>
   </div>

   <div id="blazor-error-ui">
       An unhandled error has occurred.
       <a href="" class="reload">Reload</a>
       <a class="dismiss">🗙</a>
   </div>
   <script src="@basePath/_content/BlazorMonaco/jsInterop.js"></script>
   <script src="@basePath/_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js"></script>
   <script src="@basePath/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js"></script>
   <script src="@basePath/_content/MudBlazor/MudBlazor.min.js"></script>
   <script src="@basePath/_content/CodeBeam.MudBlazor.Extensions/MudExtensions.min.js"></script>
   <script src="@basePath/_content/Radzen.Blazor/Radzen.Blazor.js"></script>
   <script>
       window.getClientConfig = function() { return {
           "apiUrl": "@apiUrl",
           "basePath": "@basePath"
        } };
   </script>
   <script src="_framework/blazor.webassembly.js"></script>
   </body>

   </html>
   ```

## Setup Client <a href="#setup-client" id="setup-client"></a>

Next, we will modify the client project.

1. **Add Elsa Studio Packages**

   Navigate to the root directory of the client project and add the following Elsa Studio packages:

   ```bash
   cd ../ElsaStudio
   dotnet add package Elsa.Studio
   dotnet add package Elsa.Studio.Core.BlazorWasm
   dotnet add package Elsa.Studio.Authentication.ElsaIdentity.BlazorWasm
   dotnet add package Elsa.Studio.Authentication.ElsaIdentity.UI
   dotnet add package Elsa.Studio.Authentication.OpenIdConnect.BlazorWasm
   dotnet add package Elsa.Studio.Localization.BlazorWasm
   dotnet add package Elsa.Api.Client
   ```
2. **Modify Program.cs**

   Open `Program.cs` and replace its existing content with the code provided below:

   Program.cs

   ```csharp
   using System.Text.Json;
   using Elsa.Studio.Authentication.ElsaIdentity.BlazorWasm.Extensions;
   using Elsa.Studio.Authentication.ElsaIdentity.HttpMessageHandlers;
   using Elsa.Studio.Authentication.ElsaIdentity.UI.Extensions;
   using Elsa.Studio.Authentication.OpenIdConnect.BlazorWasm.Extensions;
   using Elsa.Studio.Authentication.OpenIdConnect.HttpMessageHandlers;
   using Elsa.Studio.Contracts;
   using Elsa.Studio.Core.BlazorWasm.Extensions;
   using Elsa.Studio.Dashboard.Extensions;
   using Elsa.Studio.Extensions;
   using Elsa.Studio.Localization.BlazorWasm.Extensions;
   using Elsa.Studio.Localization.Models;
   using Elsa.Studio.Models;
   using Elsa.Studio.Options;
   using Elsa.Studio.Shell;
   using Elsa.Studio.Shell.Extensions;
   using Elsa.Studio.Workflows.Designer.Extensions;
   using Elsa.Studio.Workflows.Extensions;
   using Microsoft.AspNetCore.Components.Web;
   using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
   using Microsoft.Extensions.Options;
   using Microsoft.JSInterop;

   // Build the host.
   var builder = WebAssemblyHostBuilder.CreateDefault(args);
   var configuration = builder.Configuration;

   // Register root components.
   builder.RootComponents.Add<App>("#app");
   builder.RootComponents.Add<HeadOutlet>("head::after");
   builder.RootComponents.RegisterCustomElsaStudioElements();

   // Choose authentication provider.
   // Supported values: "OpenIdConnect" or "ElsaIdentity".
   var authProvider = configuration["Authentication:Provider"];
   if (string.IsNullOrWhiteSpace(authProvider))
       authProvider = "ElsaIdentity";

   Type authenticationHandler;

   if (authProvider.Equals("ElsaIdentity", StringComparison.OrdinalIgnoreCase))
   {
       // Elsa Identity (username/password against Elsa backend) + login UI at /login.
       builder.Services.AddElsaIdentity();
       builder.Services.AddElsaIdentityUI();
       authenticationHandler = typeof(ElsaIdentityAuthenticatingApiHttpMessageHandler);
   }
   else if (authProvider.Equals("OpenIdConnect", StringComparison.OrdinalIgnoreCase))
   {
       // OpenID Connect.
       builder.Services.AddOpenIdConnectAuth(options =>
       {
           configuration.GetSection("Authentication:OpenIdConnect").Bind(options);
       });
       authenticationHandler = typeof(OidcAuthenticatingApiHttpMessageHandler);
   }
   else
   {
       throw new InvalidOperationException($"Unsupported Authentication:Provider value '{authProvider}'. Supported values are 'OpenIdConnect' and 'ElsaIdentity'.");
   }

   // Register shell services and modules.
   var localizationConfig = new LocalizationConfig
   {
       ConfigureLocalizationOptions = options => configuration.GetSection("Localization").Bind(options)
   };

   builder.Services.AddCore();
   builder.Services.AddShell();
   builder.Services.AddRemoteBackend(new()
   {
       ConfigureHttpClientBuilder = options => options.AuthenticationHandler = authenticationHandler
   });

   builder.Services.AddDashboardModule();
   builder.Services.AddWorkflowsModule();
   builder.Services.AddLocalizationModule(localizationConfig);

   // Build the application.
   var app = builder.Build();

   await app.UseElsaLocalization();

   // Apply client config.
   var js = app.Services.GetRequiredService<IJSRuntime>();
   var clientConfig = await js.InvokeAsync<JsonElement>("getClientConfig");
   var apiUrl = clientConfig.GetProperty("apiUrl").GetString() ?? throw new InvalidOperationException("No API URL configured.");
   app.Services.GetRequiredService<IOptions<BackendOptions>>().Value.Url = new(apiUrl);

   // Run each startup task.
   var startupTaskRunner = app.Services.GetRequiredService<IStartupTaskRunner>();
   await startupTaskRunner.RunStartupTasksAsync();

   // Run the application.
   await app.RunAsync();
   ```
3. **Configure Client Authentication and Localization**

   The hosted page still supplies the backend API URL through `window.getClientConfig`, but the client can use `wwwroot/appsettings.json` to select the Studio authentication provider and localization settings:

   ```json
   {
       "Authentication": {
           "Provider": "OpenIdConnect",
           "OpenIdConnect": {
               "Authority": "https://login.microsoftonline.com/{tenant-id}/v2.0",
               "ClientId": "{client-id}",
               "AuthenticationScopes": [
                   "openid",
                   "profile",
                   "offline_access"
               ],
               "BackendApiScopes": [
                   "api://{backend-api-client-id}/elsa-server-api"
               ]
           }
       },
       "Localization": {
           "DefaultCulture": "en-US",
           "SupportedCultures": [
               "en-US"
           ]
       }
   }
   ```

   `AuthenticationScopes` are used during Studio sign-in. `BackendApiScopes` are used when Studio requests bearer tokens for Elsa Server API calls.

   Because this client is Blazor WebAssembly, register `{studio-url}/authentication/login-callback` as the redirect URI and `{studio-url}/authentication/logout-callback` as the logout callback URI. Studio initiates logout at `{studio-url}/authentication/logout`.
4. **Modify MainLayout.razor**

   Update `Layout/MainLayout.razor` with the following code listing:

   **MainLayout.razor**

   ```cshtml
   @inherits LayoutComponentBase

   <main>
       @Body
   </main>
   ```

## Launch the Application <a href="#run-application" id="run-application"></a>

To see your application in action, navigate back to the root directory containing the host project:

```bash
cd ../ElsaServer
```

Then execute the following command:

```bash
dotnet run --urls https://localhost:5001
```

Your application is now accessible at [https://localhost:5001](https://localhost:5001/).

By default, you can log in using:

```
username: admin
password: password
```

## Source Code <a href="#source-code" id="source-code"></a>

The source code for this chapter can be found [here](https://github.com/elsa-workflows/elsa-guides/tree/main/src/installation/elsa-server-and-studio)


# V2 to V3 Migration Guide

Complete migration guide from Elsa Workflows V2 to V3, covering breaking changes, custom activities, workflows, and concepts.

## Overview

Elsa Workflows 3 is a complete rewrite of the library with significant architectural improvements for scalability, performance, and extensibility. This guide helps you migrate from Elsa V2 to V3.

{% hint style="warning" %}
**Important:** There is no automated migration path from V2 to V3. The internal representation of workflow definitions, activities, and properties has changed substantially. Manual recreation of workflows in V3 is required.
{% endhint %}

### What's Changed

* Complete rewrite with new execution model
* New workflow JSON schema and structure
* Updated custom activity implementation
* Different NuGet package structure
* New database schema
* Improved background activity scheduler
* Enhanced blocking activities and triggers

### Migration Strategy

Given the scope of changes, consider these approaches:

1. **Parallel Operation**: Run V2 and V3 systems side-by-side, allowing V2 workflows to complete while starting new workflows in V3
2. **Incremental Migration**: Migrate workflows in phases, starting with simpler workflows
3. **Fresh Start**: For smaller implementations, recreate workflows from scratch in V3

## Migration Checklist

Use this checklist to track your migration progress:

### Preparation

* [ ] Review all existing V2 workflows and document their purposes
* [ ] Inventory custom activities and extensions
* [ ] Document integration points and external dependencies
* [ ] Set up a V3 development environment
* [ ] Review V3 documentation and new features

### Package Migration

* [ ] Update NuGet package references
* [ ] Update using statements and namespaces
* [ ] Configure NuGet feeds for preview packages (if needed)
* [ ] Resolve dependency conflicts

### Custom Activities

* [ ] Identify all custom activities in V2
* [ ] Rewrite custom activities using V3 API
* [ ] Update activity registration code
* [ ] Test custom activities in isolation
* [ ] Update metadata attributes

### Workflow Definitions

* [ ] Export V2 workflows as JSON
* [ ] Convert JSON to V3 schema format
* [ ] Update activity type names to fully qualified names
* [ ] Restructure to use root activity container
* [ ] Update expressions and property definitions
* [ ] Test each workflow individually

### Configuration

* [ ] Update startup configuration
* [ ] Migrate database connection strings
* [ ] Configure new persistence providers
* [ ] Update authentication/authorization setup
* [ ] Configure background scheduler settings

### Testing

* [ ] Create test plans for each workflow
* [ ] Validate workflow execution
* [ ] Test blocking activities and resumption
* [ ] Verify integrations with external systems
* [ ] Performance testing

### Deployment

* [ ] Plan deployment strategy (parallel vs cutover)
* [ ] Migrate or recreate database schema
* [ ] Deploy V3 application
* [ ] Monitor for issues
* [ ] Document any remaining V2 dependencies

## Breaking Changes

### NuGet Packages

#### V2 Package Structure

```xml
<PackageReference Include="Elsa.Core" Version="2.x.x" />
<PackageReference Include="Elsa.Server.Api" Version="2.x.x" />
<PackageReference Include="Elsa.Designer.Components.Web" Version="2.x.x" />
<PackageReference Include="Elsa.Persistence.EntityFramework.SqlServer" Version="2.x.x" />
```

#### V3 Package Structure

```xml
<!-- Main package includes core components -->
<PackageReference Include="Elsa" Version="3.x.x" />

<!-- Or use individual packages -->
<PackageReference Include="Elsa.Workflows.Core" Version="3.x.x" />
<PackageReference Include="Elsa.Workflows.Management" Version="3.x.x" />
<PackageReference Include="Elsa.Workflows.Runtime" Version="3.x.x" />
<PackageReference Include="Elsa.Persistence.EFCore.SqlServer" Version="3.x.x" />
```

**Key Changes:**

* Consolidated packages: The `Elsa` meta-package includes `Elsa.Api.Common`, `Elsa.Mediator`, `Elsa.Workflows.Core`, `Elsa.Workflows.Management`, and `Elsa.Workflows.Runtime`
* Persistence packages renamed: `Elsa.Persistence.EntityFramework.*` → `Elsa.Persistence.EFCore.*`
* .NET 8+ required (no longer supports .NET Standard 2.0)

### Namespace Changes

#### Common Namespace Mappings

| V2 Namespace      | V3 Namespace                                     |
| ----------------- | ------------------------------------------------ |
| `Elsa.Activities` | `Elsa.Workflows.Activities`                      |
| `Elsa.Services`   | `Elsa.Workflows.Core` / `Elsa.Workflows.Runtime` |
| `Elsa.Models`     | `Elsa.Workflows.Models`                          |
| `Elsa.Attributes` | `Elsa.Workflows.Attributes`                      |

#### Example Migration

**V2:**

```csharp
using Elsa;
using Elsa.Activities;
using Elsa.Services;
using Elsa.Attributes;
```

**V3:**

```csharp
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Attributes;
using Elsa.Extensions;
```

### Startup Configuration

#### V2 Configuration

```csharp
public void ConfigureServices(IServiceCollection services)
{
    services
        .AddElsa(elsa => elsa
            .UseEntityFrameworkPersistence(ef => ef.UseSqlServer(connectionString))
            .AddConsoleActivities()
            .AddHttpActivities()
            .AddQuartzTemporalActivities()
            .AddActivity<MyCustomActivity>()
        );
}

public void Configure(IApplicationBuilder app)
{
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}
```

#### V3 Configuration

```csharp
var builder = WebApplication.CreateBuilder(args);

// Add Elsa services
builder.Services.AddElsa(elsa => 
{
    elsa
        .UseWorkflowManagement(management => 
        {
            management.UseEntityFrameworkCore(ef => 
                ef.UseSqlServer(connectionString));
        })
        .UseWorkflowRuntime(runtime =>
        {
            runtime.UseEntityFrameworkCore(ef => 
                ef.UseSqlServer(connectionString));
        })
        .UseIdentity(identity =>
        {
            identity.UseEntityFrameworkCore(ef => 
                ef.UseSqlServer(connectionString));
        })
        .UseDefaultAuthentication()
        .UseHttp()
        .AddActivitiesFrom<Program>();
});

var app = builder.Build();

// Use Elsa middleware
app.UseWorkflowsApi();
app.UseWorkflows();

app.Run();
```

**Key Changes:**

* Separate management and runtime configuration
* Explicit middleware registration
* More granular control over features

## Custom Activities Migration

### Activity Implementation Changes

#### V2 Custom Activity

```csharp
using Elsa;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Services;
using Elsa.Services.Models;

[Activity(
    Category = "MyCategory",
    Description = "Prints a message to the console",
    Outcomes = new[] { OutcomeNames.Done }
)]
public class PrintMessage : Activity
{
    [ActivityInput(
        Label = "Message",
        Hint = "The message to print",
        SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
    )]
    public string Message { get; set; } = default!;

    protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context)
    {
        Console.WriteLine(Message);
        return Done();
    }
}
```

#### V3 Custom Activity

```csharp
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;

[Activity("MyCompany", "MyCategory", "Prints a message to the console")]
public class PrintMessage : CodeActivity
{
    [Input(Description = "The message to print.")]
    public Input<string> Message { get; set; } = default!;

    protected override void Execute(ActivityExecutionContext context)
    {
        var message = Message.Get(context);
        Console.WriteLine(message);
    }
}
```

**Key Differences:**

1. **Base Class**:
   * V2: `Activity` base class
   * V3: `Activity` or `CodeActivity` (CodeActivity auto-completes)
2. **Execute Method**:
   * V2: `OnExecute` or `OnExecuteAsync` returning `IActivityExecutionResult`
   * V3: `ExecuteAsync` or `Execute` (for CodeActivity)
3. **Completion**:
   * V2: Return `Done()`, `Outcome()`, etc.
   * V3: Call `await context.CompleteActivityAsync()` (or automatic with CodeActivity)
4. **Attributes**:
   * V2: `[Activity]` with separate Category parameter
   * V3: `[Activity]` with namespace, category, and description
5. **Input Properties**:
   * V2: Simple types with `[ActivityInput]`
   * V3: Wrapped in `Input<T>` with `[Input]`
6. **Getting Input Values**:
   * V2: Direct property access
   * V3: `Message.Get(context)`

### Activity with Outputs

#### V2 Activity with Output

```csharp
[Activity(Category = "Custom", Description = "Generates a random number")]
public class GenerateRandomNumber : Activity
{
    [ActivityOutput]
    public decimal Result { get; set; }

    protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context)
    {
        Result = Random.Shared.Next(1, 100);
        return Done();
    }
}
```

#### V3 Activity with Output

```csharp
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;

[Activity("MyCompany", "Custom", "Generates a random number")]
public class GenerateRandomNumber : CodeActivity
{
    [Output(Description = "The generated random number.")]
    public Output<decimal> Result { get; set; } = default!;

    protected override void Execute(ActivityExecutionContext context)
    {
        var randomNumber = Random.Shared.Next(1, 100);
        Result.Set(context, randomNumber);
    }
}
```

**Key Changes:**

* Outputs wrapped in `Output<T>`
* Use `Result.Set(context, value)` instead of direct assignment

### Async Activities

#### V2 Async Activity

```csharp
public class CallApiActivity : Activity
{
    [ActivityInput]
    public string Url { get; set; } = default!;

    [ActivityOutput]
    public string Response { get; set; } = default!;

    protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(
        ActivityExecutionContext context)
    {
        using var client = new HttpClient();
        Response = await client.GetStringAsync(Url);
        return Done();
    }
}
```

#### V3 Async Activity

```csharp
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Models;

[Activity("MyCompany", "Http", "Calls an HTTP API")]
public class CallApiActivity : Activity
{
    [Input(Description = "The URL to call")]
    public Input<string> Url { get; set; } = default!;

    [Output(Description = "The API response")]
    public Output<string> Response { get; set; } = default!;

    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
    {
        var url = Url.Get(context);
        using var client = context.GetRequiredService<IHttpClientFactory>().CreateClient();
        var response = await client.GetStringAsync(url);
        Response.Set(context, response);
        await context.CompleteActivityAsync();
    }
}
```

**Key Changes:**

* Method name: `OnExecuteAsync` → `ExecuteAsync`
* Must explicitly call `await context.CompleteActivityAsync()`
* Use service location via `context.GetRequiredService<T>()` instead of constructor injection

### Blocking Activities

#### V2 Blocking Activity

```csharp
public class WaitForEvent : Activity
{
    [ActivityInput]
    public string EventName { get; set; } = default!;

    protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context)
    {
        return Suspend();
    }

    protected override IActivityExecutionResult OnResume(ActivityExecutionContext context)
    {
        return Done();
    }
}
```

#### V3 Blocking Activity

```csharp
using Elsa.Workflows;
using Elsa.Workflows.Models;

[Activity("MyCompany", "Events", "Waits for an event to occur")]
public class WaitForEvent : Activity
{
    [Input(Description = "The name of the event to wait for")]
    public Input<string> EventName { get; set; } = default!;

    protected override void Execute(ActivityExecutionContext context)
    {
        var eventName = EventName.Get(context);
        context.CreateBookmark(eventName);
    }
}
```

**Key Changes:**

* V2: Return `Suspend()` to block
* V3: Call `context.CreateBookmark(payload)` to block
* V3: No separate `OnResume` method; execution continues after bookmark is resumed

### Trigger Activities

#### V2 Trigger Activity

```csharp
[Trigger(
    Category = "Custom",
    Description = "Triggers workflow when event occurs"
)]
public class MyEventTrigger : Activity
{
    [ActivityInput]
    public string EventName { get; set; } = default!;

    protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context)
    {
        return Suspend();
    }

    protected override IActivityExecutionResult OnResume(ActivityExecutionContext context)
    {
        return Done();
    }
}
```

#### V3 Trigger Activity

```csharp
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Models;

[Activity("MyCompany", "Events", "Triggers workflow when event occurs")]
public class MyEventTrigger : Trigger
{
    [Input(Description = "The name of the event")]
    public Input<string> EventName { get; set; } = default!;

    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
    {
        // If this trigger started the workflow, complete immediately
        if (context.IsTriggerOfWorkflow())
        {
            await context.CompleteActivityAsync();
            return;
        }

        // Otherwise, create a bookmark to wait
        var eventName = EventName.Get(context);
        context.CreateBookmark(eventName);
    }

    protected override object GetTriggerPayload(TriggerIndexingContext context)
    {
        var eventName = EventName.Get(context.ExpressionExecutionContext);
        return eventName;
    }
}
```

**Key Changes:**

* V3: Inherit from `Trigger` base class
* V3: Check `context.IsTriggerOfWorkflow()` to handle trigger vs resumption
* V3: Implement `GetTriggerPayload` to return the bookmark payload for indexing

### Activity Registration

#### V2 Registration

```csharp
services.AddElsa(elsa => elsa
    .AddActivity<PrintMessage>()
    .AddActivity<GenerateRandomNumber>()
    .AddActivity<CallApiActivity>()
);

// Or register all from assembly
services.AddElsa(elsa => elsa
    .AddActivitiesFrom<Startup>()
);
```

#### V3 Registration

```csharp
builder.Services.AddElsa(elsa => elsa
    .AddActivity<PrintMessage>()
    .AddActivity<GenerateRandomNumber>()
    .AddActivity<CallApiActivity>()
);

// Or register all from assembly
builder.Services.AddElsa(elsa => elsa
    .AddActivitiesFrom<Program>()
);
```

**Note:** Registration pattern is similar, but uses the new builder pattern in V3.

## Workflow JSON Migration

### V2 Workflow JSON Structure

```json
{
  "id": "workflow-1",
  "version": 1,
  "name": "Hello World Workflow",
  "description": "A simple workflow",
  "isPublished": true,
  "activities": [
    {
      "activityId": "activity-1",
      "type": "WriteLine",
      "displayName": "Write Line",
      "properties": [
        {
          "name": "Text",
          "syntax": "Literal",
          "expressions": {
            "Literal": "Hello World!"
          }
        }
      ]
    },
    {
      "activityId": "activity-2",
      "type": "Delay",
      "properties": [
        {
          "name": "Duration",
          "syntax": "Literal",
          "expressions": {
            "Literal": "00:00:01"
          }
        }
      ]
    },
    {
      "activityId": "activity-3",
      "type": "WriteLine",
      "properties": [
        {
          "name": "Text",
          "syntax": "Literal",
          "expressions": {
            "Literal": "Goodbye!"
          }
        }
      ]
    }
  ],
  "connections": [
    {
      "sourceActivityId": "activity-1",
      "targetActivityId": "activity-2",
      "outcome": "Done"
    },
    {
      "sourceActivityId": "activity-2",
      "targetActivityId": "activity-3",
      "outcome": "Done"
    }
  ]
}
```

### V3 Workflow JSON Structure

```json
{
  "id": "HelloWorld-v1",
  "definitionId": "HelloWorld",
  "name": "Hello World Workflow",
  "description": "A simple workflow",
  "version": 1,
  "isLatest": true,
  "isPublished": true,
  "root": {
    "id": "Flowchart1",
    "type": "Elsa.Flowchart",
    "version": 1,
    "activities": [
      {
        "id": "WriteLine1",
        "type": "Elsa.WriteLine",
        "version": 1,
        "name": "WriteLine1",
        "text": {
          "typeName": "String",
          "expression": {
            "type": "Literal",
            "value": "Hello World!"
          }
        }
      },
      {
        "id": "Delay1",
        "type": "Elsa.Delay",
        "version": 1,
        "name": "Delay1",
        "duration": {
          "typeName": "TimeSpan",
          "expression": {
            "type": "Literal",
            "value": "00:00:01"
          }
        }
      },
      {
        "id": "WriteLine2",
        "type": "Elsa.WriteLine",
        "version": 1,
        "name": "WriteLine2",
        "text": {
          "typeName": "String",
          "expression": {
            "type": "Literal",
            "value": "Goodbye!"
          }
        }
      }
    ],
    "connections": [
      {
        "source": {
          "activity": "WriteLine1",
          "port": "Done"
        },
        "target": {
          "activity": "Delay1",
          "port": "In"
        }
      },
      {
        "source": {
          "activity": "Delay1",
          "port": "Done"
        },
        "target": {
          "activity": "WriteLine2",
          "port": "In"
        }
      }
    ]
  }
}
```

### Key JSON Schema Changes

| Aspect                 | V2                                            | V3                                                          |
| ---------------------- | --------------------------------------------- | ----------------------------------------------------------- |
| **Root Container**     | Activities listed directly                    | Activities wrapped in `root` object (Flowchart, Sequence)   |
| **Activity Types**     | Simple names: `"WriteLine"`                   | Fully qualified: `"Elsa.WriteLine"`                         |
| **Properties**         | Array of property objects                     | Direct properties on activity with expression wrappers      |
| **Property Structure** | `properties[].name` with `expressions` object | Direct property with `typeName` and `expression`            |
| **Connections**        | `sourceActivityId` / `targetActivityId`       | Nested `source`/`target` objects with `activity` and `port` |
| **Metadata**           | Basic `id`, `version`, `isPublished`          | Additional `definitionId`, `isLatest`                       |

### Migration Steps for JSON

1. **Add Root Container**: Wrap all activities in a `root` object

   ```json
   {
     "root": {
       "type": "Elsa.Flowchart",
       "activities": [ /* your activities */ ],
       "connections": [ /* your connections */ ]
     }
   }
   ```
2. **Update Activity Type Names**: Add `Elsa.` prefix to all activity types
   * `WriteLine` → `Elsa.WriteLine`
   * `Delay` → `Elsa.Delay`
   * `HttpEndpoint` → `Elsa.HttpEndpoint`
   * `SendHttpRequest` → `Elsa.Http.SendHttpRequest`
3. **Convert Property Structure**: Transform property arrays to direct properties

   **V2:**

   ```json
   "properties": [
     {
       "name": "Text",
       "syntax": "Literal",
       "expressions": {
         "Literal": "Hello"
       }
     }
   ]
   ```

   **V3:**

   ```json
   "text": {
     "typeName": "String",
     "expression": {
       "type": "Literal",
       "value": "Hello"
     }
   }
   ```
4. **Update Connection Structure**: Change to nested source/target format

   **V2:**

   ```json
   {
     "sourceActivityId": "activity-1",
     "targetActivityId": "activity-2",
     "outcome": "Done"
   }
   ```

   **V3:**

   ```json
   {
     "source": {
       "activity": "activity-1",
       "port": "Done"
     },
     "target": {
       "activity": "activity-2",
       "port": "In"
     }
   }
   ```
5. **Add Required Metadata**: Include new V3 fields

   ```json
   {
     "definitionId": "unique-workflow-id",
     "isLatest": true
   }
   ```

### Expression Type Mapping

| V2 Expression Syntax | V3 Expression Type |
| -------------------- | ------------------ |
| `Literal`            | `Literal`          |
| `JavaScript`         | `JavaScript`       |
| `Liquid`             | `Liquid`           |
| `Json`               | `Object`           |

## Programmatic Workflows

### V2 Programmatic Workflow

```csharp
using Elsa.Activities.Console;
using Elsa.Builders;

public class HelloWorldWorkflow : IWorkflow
{
    public void Build(IWorkflowBuilder builder)
    {
        builder
            .StartWith<WriteLine>(x => x.WithText("Hello World!"))
            .Then<WriteLine>(x => x.WithText("Goodbye!"));
    }
}

// Registration
services.AddElsa(elsa => elsa
    .AddWorkflow<HelloWorldWorkflow>()
);
```

### V3 Programmatic Workflow

```csharp
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Contracts;

public class HelloWorldWorkflow : WorkflowBase
{
    protected override void Build(IWorkflowBuilder builder)
    {
        builder.Root = new Sequence
        {
            Activities =
            {
                new WriteLine("Hello World!"),
                new WriteLine("Goodbye!")
            }
        };
    }
}

// Registration
builder.Services.AddElsa(elsa => elsa
    .AddWorkflow<HelloWorldWorkflow>()
);
```

**Key Changes:**

* V2: Implement `IWorkflow` and use fluent API with `StartWith`/`Then`
* V3: Inherit from `WorkflowBase` and build activity tree directly
* V3: More explicit activity composition with `builder.Root`
* V3: Activities instantiated directly instead of using extension methods

### Workflow with Variables

#### V2 Workflow with Variables

```csharp
public class VariableWorkflow : IWorkflow
{
    public void Build(IWorkflowBuilder builder)
    {
        var counter = builder.WithVariable<int>("Counter");

        builder
            .StartWith<SetVariable>(x => x
                .WithVariableName("Counter")
                .WithValue(0))
            .Then<WriteLine>(x => x
                .WithText(context => $"Counter: {counter.Get(context)}"));
    }
}
```

#### V3 Workflow with Variables

```csharp
public class VariableWorkflow : WorkflowBase
{
    protected override void Build(IWorkflowBuilder builder)
    {
        var counter = builder.WithVariable<int>("Counter", 0);

        builder.Root = new Sequence
        {
            Activities =
            {
                new SetVariable
                {
                    Variable = counter,
                    Value = new(10)
                },
                new WriteLine
                {
                    Text = new(context => $"Counter: {counter.Get(context)}")
                }
            }
        };
    }
}
```

**Key Changes:**

* Variable declaration syntax similar but initialization improved
* Setting variables uses direct property assignment instead of fluent methods
* Accessing variables uses same `counter.Get(context)` pattern

## Database and Persistence

### Schema Changes

The database schema has changed significantly between V2 and V3:

* Table names and structures are different
* Workflow instance data serialization format changed
* No automated migration scripts available

**Recommended Approach:**

1. **Parallel Databases**: Use separate databases for V2 and V3
2. **Let V2 Complete**: Allow existing V2 workflows to finish naturally
3. **Fresh Start in V3**: Create new workflow instances in V3

{% hint style="info" %}
Database-level migration is complex and error-prone. Most teams run V2 and V3 in parallel until V2 workflows complete.
{% endhint %}

### V2 Persistence Configuration

```csharp
services.AddElsa(elsa => elsa
    .UseEntityFrameworkPersistence(ef => ef
        .UseSqlServer(connectionString)
    )
);
```

### V3 Persistence Configuration

```csharp
builder.Services.AddElsa(elsa => 
{
    elsa
        .UseWorkflowManagement(management => 
        {
            management.UseEntityFrameworkCore(ef => 
                ef.UseSqlServer(connectionString));
        })
        .UseWorkflowRuntime(runtime =>
        {
            runtime.UseEntityFrameworkCore(ef => 
                ef.UseSqlServer(connectionString));
        });
});
```

**Key Changes:**

* Separate persistence configuration for management and runtime
* Different method names: `UseEntityFrameworkPersistence` → `UseEntityFrameworkCore`
* More explicit control over what gets persisted

### Supported Providers

| Provider   | V2 Package                                    | V3 Package                           |
| ---------- | --------------------------------------------- | ------------------------------------ |
| SQL Server | `Elsa.Persistence.EntityFramework.SqlServer`  | `Elsa.Persistence.EFCore.SqlServer`  |
| PostgreSQL | `Elsa.Persistence.EntityFramework.PostgreSql` | `Elsa.Persistence.EFCore.PostgreSql` |
| MySQL      | `Elsa.Persistence.EntityFramework.MySql`      | `Elsa.Persistence.EFCore.MySql`      |
| SQLite     | `Elsa.Persistence.EntityFramework.Sqlite`     | `Elsa.Persistence.EFCore.Sqlite`     |
| MongoDB    | `Elsa.Persistence.MongoDb`                    | `Elsa.MongoDb`                       |

## Background Job Scheduler

### V2 Job Scheduling

In V2, background activities required external job scheduler like Hangfire:

```csharp
services.AddElsa(elsa => elsa
    .UseQuartzTemporalActivities()
    .AddActivity<LongRunningTask>()
);

// Configure Hangfire
services.AddHangfire(config => config
    .UseSqlServerStorage(connectionString));
```

### V3 Job Scheduling

V3 includes a built-in background activity scheduler using .NET Channels:

```csharp
// Background scheduling is included by default
builder.Services.AddElsa(elsa => 
{
    elsa
        .UseWorkflowRuntime(runtime =>
        {
            // Configure background activity scheduler options if needed
            runtime.ConfigureBackgroundActivityScheduler(options =>
            {
                options.MaxConcurrentActivities = 10;
            });
        });
});

// Mark activities for background execution
[Activity("MyCompany", "Tasks", "Long running task", Kind = ActivityKind.Job)]
public class LongRunningTask : CodeActivity
{
    protected override void Execute(ActivityExecutionContext context)
    {
        // Long-running work
    }
}
```

**Key Changes:**

* Built-in scheduler eliminates need for Hangfire in basic scenarios
* Use `ActivityKind.Job` for background activities
* Hangfire still supported for advanced scenarios (failover, distributed execution)

## Common Migration Pitfalls

### 1. Direct JSON Import

**Problem:** Attempting to import V2 workflow JSON directly into V3.

**Solution:** V2 and V3 use incompatible JSON schemas. You must:

* Export V2 workflows as JSON
* Transform the JSON structure to V3 format
* Update all activity type names to fully qualified names
* Test thoroughly before importing

### 2. Assuming API Compatibility

**Problem:** Expecting V2 APIs to work in V3 with minor changes.

**Solution:** V3 is a complete rewrite. Expect to rewrite:

* Custom activities completely
* Workflow definitions
* Integration points
* Extension implementations

### 3. Database Migration

**Problem:** Trying to migrate the database schema from V2 to V3.

**Solution:**

* Use separate databases for V2 and V3
* Run systems in parallel during transition
* Let V2 workflows complete naturally
* Start new workflows in V3

### 4. Constructor Injection in Activities

**Problem:** Using constructor injection in custom activities.

**V2 Pattern (worked but discouraged):**

```csharp
public class MyActivity : Activity
{
    private readonly IMyService _service;
    
    public MyActivity(IMyService service)
    {
        _service = service;
    }
}
```

**V3 Solution (service location):**

```csharp
public class MyActivity : CodeActivity
{
    protected override void Execute(ActivityExecutionContext context)
    {
        var service = context.GetRequiredService<IMyService>();
        // Use service
    }
}
```

**Reason:** Service location makes activity instantiation easier in workflow definitions.

### 5. Forgetting Activity Completion

**Problem:** Not calling `CompleteActivityAsync` in V3 activities.

**Incorrect:**

```csharp
public class MyActivity : Activity
{
    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
    {
        Console.WriteLine("Done");
        // Missing completion!
    }
}
```

**Correct:**

```csharp
public class MyActivity : Activity
{
    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
    {
        Console.WriteLine("Done");
        await context.CompleteActivityAsync(); // Required!
    }
}
```

**Or use CodeActivity:**

```csharp
public class MyActivity : CodeActivity
{
    protected override void Execute(ActivityExecutionContext context)
    {
        Console.WriteLine("Done");
        // Auto-completes!
    }
}
```

### 6. Input/Output Property Access

**Problem:** Accessing `Input<T>` and `Output<T>` properties directly.

**Incorrect:**

```csharp
public class MyActivity : CodeActivity
{
    public Input<string> Message { get; set; } = default!;
    
    protected override void Execute(ActivityExecutionContext context)
    {
        Console.WriteLine(Message); // Wrong!
    }
}
```

**Correct:**

```csharp
public class MyActivity : CodeActivity
{
    public Input<string> Message { get; set; } = default!;
    
    protected override void Execute(ActivityExecutionContext context)
    {
        var message = Message.Get(context); // Correct!
        Console.WriteLine(message);
    }
}
```

### 7. Bookmark Resumption

**Problem:** Using V2 bookmark/resumption patterns in V3.

**V2 Pattern:**

```csharp
// Creating bookmark
protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context)
{
    return Suspend();
}

// Resuming
protected override IActivityExecutionResult OnResume(ActivityExecutionContext context)
{
    return Done();
}
```

**V3 Pattern:**

```csharp
// Creating bookmark
protected override void Execute(ActivityExecutionContext context)
{
    context.CreateBookmark("MyBookmark");
}

// No separate resume method - execution continues after bookmark
```

### 8. Missing Root Container in JSON

**Problem:** V2-style JSON without root container fails to parse in V3.

**Solution:** Always wrap activities in a root container:

```json
{
  "root": {
    "type": "Elsa.Flowchart",
    "activities": [ /* activities here */ ]
  }
}
```

### 9. Incorrect Package References

**Problem:** Mixing V2 and V3 packages.

**Solution:** Ensure all Elsa packages are V3:

```xml
<!-- All packages should be version 3.x -->
<PackageReference Include="Elsa" Version="3.x.x" />
<PackageReference Include="Elsa.Persistence.EFCore.SqlServer" Version="3.x.x" />
```

### 10. Trigger Activity Implementation

**Problem:** Not checking `IsTriggerOfWorkflow()` in trigger activities.

**Incorrect:**

```csharp
public class MyTrigger : Trigger
{
    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
    {
        context.CreateBookmark("MyTrigger"); // Always blocks!
    }
}
```

**Correct:**

```csharp
public class MyTrigger : Trigger
{
    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
    {
        if (context.IsTriggerOfWorkflow())
        {
            await context.CompleteActivityAsync();
            return;
        }
        
        context.CreateBookmark("MyTrigger");
    }
    
    protected override object GetTriggerPayload(TriggerIndexingContext context)
    {
        return "MyTrigger";
    }
}
```

## Testing and Validation

### Testing Strategy

1. **Unit Test Custom Activities**

   ```csharp
   [Fact]
   public async Task PrintMessage_Should_Write_To_Console()
   {
       // Arrange
       var services = new ServiceCollection()
           .AddElsa()
           .BuildServiceProvider();
       
       var activityExecutor = services.GetRequiredService<IActivityExecutor>();
       var activity = new PrintMessage
       {
           Message = new Input<string>("Hello")
       };
       
       // Act
       var result = await activityExecutor.ExecuteAsync(activity);
       
       // Assert
       Assert.True(result.IsCompleted);
   }
   ```
2. **Integration Test Workflows**

   ```csharp
   [Fact]
   public async Task Workflow_Should_Execute_Successfully()
   {
       // Arrange
       var services = new ServiceCollection()
           .AddElsa(elsa => elsa.AddWorkflow<HelloWorldWorkflow>())
           .BuildServiceProvider();
       
       var workflowRunner = services.GetRequiredService<IWorkflowRunner>();
       
       // Act
       var result = await workflowRunner.RunAsync<HelloWorldWorkflow>();
       
       // Assert
       Assert.Equal(WorkflowStatus.Finished, result.Status);
   }
   ```
3. **Test JSON Workflows**

   ```csharp
   [Fact]
   public async Task Should_Load_And_Execute_JSON_Workflow()
   {
       var json = File.ReadAllText("workflow.json");
       var services = new ServiceCollection()
           .AddElsa()
           .BuildServiceProvider();
       
       var serializer = services.GetRequiredService<IActivitySerializer>();
       var workflowDefinitionModel = serializer.Deserialize<WorkflowDefinitionModel>(json);
       var workflowDefinitionMapper = services.GetRequiredService<WorkflowDefinitionMapper>();
       var workflow = workflowDefinitionMapper.Map(workflowDefinitionModel);
       
       var runner = services.GetRequiredService<IWorkflowRunner>();
       var result = await runner.RunAsync(workflow);
       
       Assert.Equal(WorkflowStatus.Finished, result.Status);
   }
   ```

### Validation Checklist

* [ ] All custom activities compile and run
* [ ] Activity inputs and outputs work correctly
* [ ] Blocking activities create bookmarks properly
* [ ] Trigger activities can start workflows
* [ ] Workflows execute from start to finish
* [ ] Workflow variables persist correctly
* [ ] Error handling works as expected
* [ ] Integration points function correctly
* [ ] Performance meets requirements
* [ ] Database persistence works

## Migration Timeline Example

### Phase 1: Preparation (Week 1-2)

* Set up V3 development environment
* Inventory all V2 workflows and custom activities
* Review V3 documentation
* Create proof-of-concept migrations

### Phase 2: Custom Activities (Week 3-4)

* Rewrite all custom activities for V3
* Unit test each activity
* Register activities with V3 engine

### Phase 3: Workflow Migration (Week 5-8)

* Convert workflow definitions to V3
* Test each workflow individually
* Migrate programmatic workflows
* Update integration points

### Phase 4: Infrastructure (Week 9-10)

* Set up V3 database schema
* Configure persistence providers
* Deploy V3 application to staging
* Configure monitoring and logging

### Phase 5: Parallel Operation (Week 11-12)

* Run V2 and V3 side-by-side
* Monitor both systems
* Route new workflows to V3
* Allow V2 workflows to complete

### Phase 6: Cutover (Week 13)

* Verify all V2 workflows completed
* Decommission V2 system
* Full production deployment of V3
* Monitor and optimize

## Resources

### Documentation

* [Elsa Workflows V3 Documentation](https://docs.elsaworkflows.io/)
* [Elsa Workflows V2 Documentation](https://v2.elsaworkflows.io/)
* [Custom Activities Guide](/extensibility/custom-activities)
* [Loading Workflows from JSON](/guides/loading-workflows-from-json)

### GitHub Resources

* [Elsa Core Repository](https://github.com/elsa-workflows/elsa-core)
* [Migration Discussion](https://github.com/elsa-workflows/elsa-core/discussions/4767)
* [Breaking Changes](https://github.com/elsa-workflows/elsa-core/releases)

### Community

* [GitHub Discussions](https://github.com/elsa-workflows/elsa-core/discussions)
* [Discord Server](https://discord.gg/hhChk5H472)

## Summary

Migrating from Elsa V2 to V3 requires significant effort due to the complete rewrite:

**Key Takeaways:**

1. ✅ No automated migration path exists
2. ✅ Custom activities must be rewritten
3. ✅ Workflow JSON must be transformed
4. ✅ Database schemas are incompatible
5. ✅ Plan for parallel operation during transition
6. ✅ V3 offers significant improvements in scalability and performance
7. ✅ Built-in background scheduler eliminates need for Hangfire in basic scenarios
8. ✅ More explicit and type-safe API

**Migration Approach:**

* Start with a comprehensive inventory of V2 assets
* Rewrite custom activities using V3 patterns
* Transform workflow definitions to V3 format
* Run V2 and V3 in parallel during transition
* Thoroughly test all migrated components
* Monitor carefully during cutover

While migration requires significant effort, V3's improvements in architecture, performance, and extensibility make it worthwhile for long-term success.


# Architecture

High-level overview of Elsa Workflows v3 architecture, covering workflow execution flow, core concepts like bookmarks and triggers, workflow runtimes, and multitenancy.

This guide provides a high-level understanding of Elsa's architecture and what happens when a workflow executes. Whether you're extending Elsa, troubleshooting issues, or simply curious about how everything fits together, this overview will help you understand the system's core concepts and structure.

## What Happens When a Workflow Executes?

At a high level, when a workflow executes in Elsa:

1. **Execute or Dispatch**: A workflow is executed via `IWorkflowRunner` or dispatched for execution via `IWorkflowDispatcher`, which enqueues the request.
2. **Load**: The workflow definition is loaded from storage (or cache)
3. **Instantiate**: A workflow instance is created or resumed (if continuing from a bookmark)
4. **Execute**: Activities run sequentially or in parallel based on the workflow structure
5. **Bookmark (Optional)**: If an activity creates a bookmark, and no more activities remain on the internal queue, execution pauses and state is persisted
6. **Complete or Suspend**: The workflow either completes or suspends, waiting for external stimuli to resume

This lifecycle repeats as workflows are triggered, execute activities, wait for events, and continue execution.

## Core Concepts

### Execute vs Dispatch

Understanding the difference between **executing** and **dispatching** workflows is fundamental:

#### Execute (IWorkflowRunner)

* **Direct, synchronous execution** of a workflow in the current process
* No queuing or background processing
* Useful for testing, simple workflows, or when immediate results are needed
* Does not involve `IWorkflowDispatcher`

```csharp
// Direct execution
var result = await workflowRunner.RunAsync(
    new RunWorkflowRequest 
    { 
        DefinitionId = workflowId 
    });
```

#### Dispatch (IWorkflowDispatcher)

* **Asynchronous, queue-based** workflow execution
* Requests are enqueued and processed by background workers
* Supports distributed execution across multiple nodes
* Default approach for production workflows

```csharp
// Dispatching for background execution
await workflowDispatcher.DispatchAsync(
    new DispatchWorkflowDefinitionRequest 
    { 
        DefinitionId = workflowId 
    });
```

For the release-backed lifecycle that connects execution, dispatch, triggers, bookmarks, persistence, and resume behavior, see [Execution Model](/guides/architecture/execution-model). For a deeper dive into dispatching, see [Workflow Dispatcher Architecture](/guides/architecture/workflow-dispatcher). For transactional delivery of dispatches made during workflow execution, see [Workflow Dispatch Outbox](/guides/architecture/workflow-dispatch-outbox).

#### IWorkflowRuntime

* **High-level abstraction** that combines runtime management with persistence
* Provides client API for workflow operations (start, resume, cancel, etc.)
* Used by most applications for workflow lifecycle management

### Bookmarks, Triggers, and Stimuli

These three concepts work together to enable event-driven, long-running workflows:

#### Bookmarks

A **bookmark** is a "pause point" in a workflow. When an activity creates a bookmark:

* The workflow's current state is persisted
* Execution suspends at that activity
* The bookmark waits for a matching stimulus to resume execution

Think of bookmarks as save points in a video game - the workflow can be resumed from exactly where it left off.

**Common activities that create bookmarks:**

* `Event` - Waits for a named event
* `Delay` - Waits for a timer
* `HTTP Endpoint` - Waits for an HTTP request
* `Send HTTP Request` (when configured to wait for response)

#### Triggers

A **trigger** is a special type of activity that starts a workflow automatically when certain conditions are met. Triggers:

* Create bookmarks when the workflow is published
* Listen for external events (HTTP requests, timers, messages, etc.)
* Automatically start new workflow instances when triggered
* Are typically the first activity in a workflow

**Example triggers:**

* `HTTP Endpoint` - Starts workflow when an HTTP request arrives
* `Timer` - Starts workflow on a schedule (cron, interval, etc.)
* `Message Received` - Starts workflow when a message arrives

#### Stimuli

A **stimulus** is an external event that resumes a suspended workflow. Stimuli:

* Are dispatched into the system via `IWorkflowDispatcher`
* Match against existing bookmarks
* Resume workflows from their bookmarked position

The relationship:

1. Activity creates a **bookmark** (workflow pauses)
2. External event occurs and generates a **stimulus**
3. Stimulus matches bookmark and resumes execution

```
Workflow -> [Activity] -> Bookmark Created -> Persisted
                                    ↓
External Event -> Stimulus Dispatched -> Bookmark Matched -> Workflow Resumes
```

### Workflow Runtimes

The **workflow runtime** is the execution environment that manages workflow lifecycle and state. Key responsibilities:

#### State Management

* Tracks workflow instances and their current state
* Persists bookmarks and activity data
* Manages workflow variables and outputs

#### Execution Coordination

* Schedules activities for execution
* Handles activity outcomes and connections
* Manages parallel execution paths

#### Event Processing

* Receives and routes stimuli to bookmarks
* Handles workflow triggers
* Coordinates distributed execution

#### Integration Points

The runtime integrates with several subsystems:

* **Persistence**: Storage for workflow definitions and instances
* **Dispatcher**: Queuing and background execution
* **Activity Registry**: Discovery of available activities
* **Expression Evaluators**: Dynamic value resolution

### Multitenancy (Conceptual Overview)

Elsa supports **multitenancy** - running multiple isolated tenants in a single deployment:

#### Tenant Isolation

* Each tenant has isolated workflow definitions, instances, and data
* Tenants cannot access each other's workflows or data
* Tenant context is established early in the request pipeline

#### Tenant Resolution

* Tenants are identified via tenant ID (from headers, routes, claims, etc.)
* Tenant resolver strategies can be customized
* Default tenant is available for single-tenant scenarios

#### Use Cases

* **SaaS Applications**: Each customer is a tenant with isolated workflows
* **Multi-Organization**: Departments or divisions with separate workflows
* **Development/Staging/Production**: Logical separation within a deployment

For detailed multitenancy setup, see the [Multitenancy Introduction](/multitenancy/introduction) guide.

## System Architecture (Mindmap)

Here's a textual representation of Elsa's core functionality and surrounding modules:

```
Elsa Workflows v3
│
├── Core Engine
│   ├── IWorkflowRunner (Direct execution)
│   ├── IWorkflowRuntime (Runtime management + persistence)
│   ├── IWorkflowDispatcher (Queue-based execution)
│   ├── Activity Registry (Available activities)
│   ├── Workflow Definition Store (Workflow definitions)
│   └── Workflow Instance Store (Running/suspended workflows)
│
├── Execution Model
│   ├── Bookmarks (Pause points)
│   ├── Triggers (Auto-start workflows)
│   ├── Stimuli (Resume signals)
│   └── Activity Execution Context
│
├── Extensibility
│   ├── Modules & Features (Plugin architecture)
│   ├── Custom Activities (Domain-specific operations)
│   ├── Expression Evaluators (C#, JavaScript, Liquid, Python)
│   └── Middleware Pipeline (Request/response interception)
│
├── Persistence Layer
│   ├── Entity Framework Core (SQL Server, PostgreSQL, SQLite, MySQL)
│   ├── MongoDB (Document store)
│   ├── Dapper (Lightweight SQL)
│   └── In-Memory (Testing/development)
│
├── Integration Packages
│   ├── HTTP (REST APIs, webhooks)
│   ├── Email (SMTP, SendGrid, etc.)
│   ├── MassTransit (Message bus integration)
│   ├── Timers (Scheduled execution)
│   └── JavaScript/C# (Scripting)
│
├── Elsa Server (Backend API)
│   ├── REST API (Workflow management, execution control)
│   ├── WebSocket (Real-time updates)
│   ├── Authentication & Authorization
│   └── Multitenancy Support
│
├── Elsa Studio (Frontend UI)
│   ├── Workflow Designer (Visual editor)
│   ├── Activity Property Editors
│   ├── Instance Monitoring
│   └── Configuration UI
│
└── Deployment & Scaling
    ├── Distributed Hosting (Multiple nodes)
    ├── Clustering (Shared state)
    ├── Background Workers (Queue processing)
    └── Load Balancing (Request distribution)
```

## Key Packages and Their Roles

| Package                         | Purpose                                                     |
| ------------------------------- | ----------------------------------------------------------- |
| **Elsa.Workflows.Core**         | Core workflow engine, activities, and abstractions          |
| **Elsa.Workflows.Runtime**      | Runtime services for execution, persistence, and management |
| **Elsa**                        | Meta-package that includes commonly needed packages         |
| **Elsa.Persistence.EFCore**     | EF Core persistence providers                               |
| **Elsa.Http**                   | HTTP activities and triggers                                |
| **Elsa.MassTransit**            | Message bus integration                                     |
| **Elsa.Expressions.JavaScript** | JavaScript expression evaluator                             |
| **Elsa.Expressions.CSharp**     | C# expression evaluator                                     |
| **Elsa.Expressions.Liquid**     | Liquid template expression evaluator                        |
| **Elsa.Alterations**            | Workflow alteration and migration support                   |

## Typical Workflow Execution Flow

Here's a detailed look at what happens during a typical workflow execution:

### 1. Trigger Event

```
HTTP Request -> Elsa Server -> Trigger Matched -> Dispatch Workflow
```

### 2. Workflow Dispatch

```
IWorkflowDispatcher.DispatchAsync()
    ↓
Enqueue Request -> Background Worker Picks Up
    ↓
Load Workflow Definition -> Create/Resume Instance
```

### 3. Activity Execution

```
For each activity in sequence:
    ↓
Load Activity Definition
    ↓
Evaluate Input Expressions (variables, outputs, etc.)
    ↓
Execute Activity Logic
    ↓
Process Outcomes (determine next activities)
    ↓
[If bookmark created] -> Persist State & Suspend
[Otherwise] -> Continue to next activity
```

### 4. Completion or Suspension

```
[All activities complete]
    ↓
Workflow Status = Completed
    ↓
Persist Final State -> Trigger Completion Events

[Bookmark created]
    ↓
Workflow Status = Suspended
    ↓
Persist Bookmark & State -> Wait for Stimulus
```

### 5. Resume from Bookmark (if suspended)

```
External Event -> Stimulus Dispatched
    ↓
Match Bookmark -> Load Workflow Instance
    ↓
Resume from Bookmarked Activity
    ↓
Continue Execution (goto step 3)
```

## Understanding the Module System

Elsa's architecture is built around a **module and feature system** that enables clean extensibility:

### Modules

A **module** is a container for related features. The `IModule` interface represents a configuration point where features can be registered and configured.

### Features

A **feature** is a self-contained unit of functionality that:

* Registers services with dependency injection
* Adds activities to the activity registry
* Configures workflow options
* Can depend on other features

### Registration Pattern

Features are registered using a fluent API:

```csharp
builder.Services.AddElsa(elsa => elsa
    .UseWorkflowRuntime()
    .UseHttp()
    .UseEmail()
    .UseJavaScript()
);
```

Each `UseXyz()` method adds a feature to the module, which then configures the necessary services.

For detailed information on creating custom modules and plugins, see the [Modules and Plugins](/guides/modules-and-plugins) guide.

## Further Reading

To dive deeper into specific aspects of Elsa's architecture:

* [**Workflow Dispatcher Architecture**](/guides/architecture/workflow-dispatcher) - Deep dive into `IWorkflowDispatcher` and dispatching patterns
* [**Workflow Dispatch Outbox**](/guides/architecture/workflow-dispatch-outbox) - Coordinate in-workflow dispatch with committed state and recover delayed delivery
* [**Modules and Plugins**](/guides/modules-and-plugins) - Learn how to extend Elsa with custom modules and activities
* [**Custom Activities**](/extensibility/custom-activities) - Create domain-specific activities
* [**Multitenancy Setup**](/multitenancy/setup) - Configure multitenancy in your application
* [**Clustering**](/guides/clustering) - Scale Elsa across multiple nodes
* [**Persistence Strategies**](/guides/persistence) - Choose and configure a persistence provider

## Summary

Elsa's architecture is designed for flexibility and scalability:

* **Multiple execution models** (direct, runtime, dispatched) for different scenarios
* **Event-driven patterns** (bookmarks, triggers, stimuli) for long-running workflows
* **Modular design** enabling clean extensibility
* **Multi-persistence support** for different storage needs
* **Multitenancy** for SaaS and multi-organization deployments

Understanding these core concepts will help you effectively build, extend, and troubleshoot Elsa-based workflow solutions.


# Execution Model

Release-backed guide to Elsa's execution model in 3.8.0, covering direct execution, dispatched execution, triggers, bookmarks, stimuli, persistence, and recovery.

This guide explains how Elsa `3.8.0` starts workflows, pauses them, resumes them, and persists their state. It is intended for both developers wiring Elsa into an application and Studio users who need a reliable mental model for what happens after a workflow is published or executed.

If you want the broad platform picture first, read the [Architecture](/guides/architecture) overview. If you need activity-level waiting and resume patterns, read [Long-Running Workflows](/guides/running-workflows/long-running-workflows).

## The four execution paths

Elsa uses four closely related runtime paths:

| Path                | Primary service                           | What it does                                                                   | Best for                                             |
| ------------------- | ----------------------------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------- |
| Direct execution    | `IWorkflowRunner`                         | Runs a workflow in-process immediately                                         | tests, simple in-process execution, custom host code |
| Runtime client      | `IWorkflowRuntime`                        | Creates workflow clients that start, run, and resume persisted instances       | most application code                                |
| Background dispatch | `IWorkflowDispatcher`                     | Queues definition, instance, trigger, and resume work for background execution | server-side and distributed execution                |
| Stimulus delivery   | `IStimulusSender` / `IStimulusDispatcher` | Matches external stimuli to triggers and bookmarks                             | HTTP, timers, messages, events, callbacks            |

In practice:

* `IWorkflowRunner` is the low-level "run now" path.
* `IWorkflowRuntime` is the higher-level operational API.
* `IWorkflowDispatcher` is the asynchronous queueing boundary.
* stimuli are how external events start new instances or resume waiting ones.

## Start, run, pause, resume

The normal lifecycle in `3.8.0` looks like this:

1. A workflow definition is published.
2. Elsa indexes startable trigger activities from the published definition.
3. A workflow is started directly, dispatched, or matched by a trigger.
4. Activities execute until the workflow finishes or an activity creates a bookmark.
5. The commit-state handler persists bookmarks, variables, execution logs, and workflow state.
6. Later, a matching stimulus resumes the workflow from the stored bookmark.

This cycle can repeat many times for one workflow instance.

## Execute versus dispatch

### Execute

`IWorkflowRunner` builds a `WorkflowExecutionContext`, schedules the workflow, and runs it in the current process. It is the most direct path and does not queue work.

Use direct execution when:

* you are writing tests
* you need an immediate result in the same process
* you are running a workflow without background infrastructure

### Dispatch

`IWorkflowDispatcher` is the queueing abstraction. In `3.8.0`, it supports four request types:

* `DispatchWorkflowDefinitionRequest` to start a new instance from a definition
* `DispatchWorkflowInstanceRequest` to continue an existing instance
* `DispatchTriggerWorkflowsRequest` to start workflows from a trigger stimulus
* `DispatchResumeWorkflowsRequest` to resume workflows waiting on bookmarks

The default `BackgroundWorkflowDispatcher` sends these requests to background command handling. When dispatch happens during workflow execution and transactional outbox support is enabled, `TransactionalWorkflowDispatcher` writes the work to Elsa's workflow dispatch outbox first, then hands it off after state commit.

Use dispatch when:

* the caller should return before workflow work completes
* you need queue-based orchestration that can be combined with persistent runtime storage and distributed hosting
* child workflow execution should not be tied to the current request lifetime

For lower-level dispatch details, see [Workflow Dispatcher Architecture](/guides/architecture/workflow-dispatcher).

## Triggers, bookmarks, and stimuli

These three concepts are the core of Elsa's event-driven model.

### Triggers start new workflow instances

In `3.8.0`, Elsa indexes triggers from published workflow definitions using `TriggerIndexer`. Two conditions matter:

* the activity must implement `ITrigger`
* the activity must be marked `CanStartWorkflow`

That means not every blocking activity automatically becomes a start trigger. For example, timer-style activities only start new workflow instances when the workflow designer or code marks them as startable.

The stored trigger record contains the workflow definition/version IDs, the activity ID, a trigger name, an optional payload, and a deterministic hash.

For Studio users, this is the practical rule:

* publishing a workflow makes Elsa index its start triggers
* unpublishing or changing the workflow causes trigger reindexing
* if a workflow does not start from an expected trigger, first confirm the activity is configured as a start trigger

### Bookmarks pause existing workflow instances

Bookmarks belong to workflow instances, not workflow definitions. Activities create them through `ActivityExecutionContext.CreateBookmark(...)`.

When a bookmark is created, Elsa:

* captures the bookmark name, payload, hash, activity ID, activity node ID, and activity instance ID
* adds the bookmark to the execution context
* persists it during state commit

`CreateBookmark(...)` hashes the bookmark using the bookmark name, the payload, and optionally the activity instance ID. That is why resume payload shape must match what the activity originally stored.

Use bookmarks when a workflow instance should wait for:

* an HTTP callback or approval link
* an event or signal
* a timer or scheduled wake-up
* a message from another system
* a background task completion signal

### Stimuli either start or resume work

Stimuli are the external inputs Elsa tries to match against stored triggers and bookmarks.

In `3.8.0`, `StimulusSender` first tries to start new workflows when the stimulus is not scoped to a specific existing instance or activity. It then tries to resume matching bookmarks.

That means one incoming event can do both:

* start new instances from trigger definitions
* resume existing suspended instances

If no bookmark matches, Elsa enqueues the stimulus in the bookmark queue as a reliability measure. The recurring `TriggerBookmarkQueueRecurringTask` keeps signaling the queue worker so recently created bookmarks can still pick up stimuli that arrived slightly too early.

## What gets persisted

In `3.8.0`, bookmark persistence is no longer handled by the old bookmark middleware. `DefaultCommitStateHandler` now commits runtime state.

On commit, Elsa persists:

* bookmark changes
* activity execution logs
* workflow execution logs
* persisted variables
* the workflow instance state itself

After saving state, Elsa emits `WorkflowStateCommitted`, which is also the hook used to process deferred dispatch outbox work.

For operators, the key implication is simple: if you want workflows to survive restarts, resumes, and delayed callbacks, configure runtime persistence rather than relying on in-memory-only execution.

## How resume matching works

When Elsa resumes bookmarks, `WorkflowResumer`:

1. builds a bookmark filter from bookmark ID or from hashed stimulus data
2. acquires a distributed lock for that filter
3. loads matching bookmarks
4. creates a workflow client for each workflow instance
5. runs the instance from the matched bookmark

This lock is important in clustered deployments because it reduces duplicate resume attempts across nodes.

## How to choose the right model

| Need                                                              | Use                            | Why                                                  |
| ----------------------------------------------------------------- | ------------------------------ | ---------------------------------------------------- |
| Run a workflow immediately in process                             | `IWorkflowRunner`              | simplest path, no queue boundary                     |
| Start or resume managed persisted workflows from application code | `IWorkflowRuntime` client API  | higher-level operational API                         |
| Queue work for background execution                               | `IWorkflowDispatcher`          | decouples execution from caller lifetime             |
| React to external events                                          | trigger plus stimulus delivery | starts new instances automatically                   |
| Pause and continue later                                          | bookmark-based activity        | persists wait state and resumes on matching stimulus |

## Practical examples

### Start immediately in code

```csharp
var result = await workflowRunner.RunAsync(workflow);
```

### Queue a new workflow instance

```csharp
await workflowDispatcher.DispatchAsync(new DispatchWorkflowDefinitionRequest(definitionVersionId)
{
    CorrelationId = orderId,
    Input = new Dictionary<string, object>
    {
        ["OrderId"] = orderId
    }
});
```

### Resume workflows waiting on a known stimulus

```csharp
await workflowDispatcher.DispatchAsync(new DispatchResumeWorkflowsRequest(
    activityTypeName: "OrderApprovedActivityType",
    bookmarkPayload: new { OrderId = orderId }));
```

The `activityTypeName` must match the bookmark-producing activity type or bookmark name used when the workflow paused.

## Common mistakes

* Treating every blocking activity as a start trigger. In `3.8.0`, start triggers must be both `ITrigger` activities and marked `CanStartWorkflow`.
* Using in-memory-only runtime services for workflows that must survive process restarts.
* Sending a resume payload that does not match the original bookmark payload shape, causing the hash lookup to miss.
* Assuming dispatch means "already executed". Dispatch only means the request has been queued successfully.
* Expecting an unpublished workflow definition to receive trigger traffic.

## Related guides

* [Architecture](/guides/architecture)
* [Workflow Dispatcher Architecture](/guides/architecture/workflow-dispatcher)
* [Long-Running Workflows](/guides/running-workflows/long-running-workflows)
* [Using a Trigger](/guides/running-workflows/using-a-trigger)
* [Timer and Scheduled Workflows](/guides/running-workflows/timer-and-scheduled-workflows)
* [Workflow Context](/getting-started/concepts/workflow-context)


# Workflow Dispatcher Architecture

Deep dive into IWorkflowDispatcher: the core dispatching abstraction for queuing and executing workflows, covering request types, event ordering, and custom dispatcher implementations.

{% hint style="info" %}
**Note**: This guide provides a deep dive into workflow dispatching. For a broader understanding of Elsa's architecture and how dispatching fits into the overall system, see the [Architecture Overview](/guides/architecture).
{% endhint %}

## Overview

The `IWorkflowDispatcher` is Elsa's core abstraction for **enqueuing and dispatching** workflows for execution. It provides a flexible mechanism to start, resume, and trigger workflows, with support for both in-process and distributed execution scenarios.

Understanding the dispatcher is crucial for:

* **Custom execution strategies**: Implementing background processing, queueing, or distributed workflows
* **Event-driven architectures**: Understanding how triggers and bookmarks flow through the system
* **Multi-process deployments**: Coordinating workflow execution across multiple nodes
* **Debugging and troubleshooting**: Knowing the sequence of events during workflow execution

## IWorkflowDispatcher vs IWorkflowRunner vs IWorkflowRuntime

Before diving into the dispatcher, it's important to understand how it relates to other workflow execution services:

| Service                 | Purpose                              | Execution Model                        | Use Case                                                                |
| ----------------------- | ------------------------------------ | -------------------------------------- | ----------------------------------------------------------------------- |
| **IWorkflowRunner**     | Direct, in-process execution         | Synchronous, immediate                 | Testing, simple workflows, in-process scenarios                         |
| **IWorkflowRuntime**    | Runtime abstraction with persistence | Async, with persistence and client API | Most application scenarios, managed execution                           |
| **IWorkflowDispatcher** | Dispatching and queuing abstraction  | Async, queue-based                     | Background processing, distributed systems, custom execution strategies |

### When to Use Each

* **IWorkflowRunner**: Use when you need immediate, synchronous execution in the same process. Ideal for unit tests or simple, non-persistent workflows.
* **IWorkflowRuntime**: Use for most application scenarios where you need workflow persistence, state management, and the ability to resume workflows. Provides a high-level client API for workflow operations.
* **IWorkflowDispatcher**: Use when you need custom control over how workflows are queued and executed, or when building distributed/multi-process architectures. Also used internally by triggers and the runtime.

## IWorkflowDispatcher Interface

The `IWorkflowDispatcher` defines four primary dispatch methods, each handling a different workflow execution scenario:

```csharp
public interface IWorkflowDispatcher
{
    Task<DispatchWorkflowDefinitionResponse> DispatchAsync(
        DispatchWorkflowDefinitionRequest request, 
        CancellationToken cancellationToken = default);
    
    Task<DispatchWorkflowInstanceResponse> DispatchAsync(
        DispatchWorkflowInstanceRequest request, 
        CancellationToken cancellationToken = default);
    
    Task<DispatchTriggerWorkflowsResponse> DispatchAsync(
        DispatchTriggerWorkflowsRequest request, 
        CancellationToken cancellationToken = default);
    
    Task<DispatchResumeWorkflowsResponse> DispatchAsync(
        DispatchResumeWorkflowsRequest request, 
        CancellationToken cancellationToken = default);
}
```

## Dispatch Request Types

### 1. DispatchWorkflowDefinitionRequest

**Purpose**: Start a new workflow instance from a workflow definition.

**Use Cases**:

* Starting a workflow via REST API
* Programmatically creating and starting workflows
* Batch processing where each item starts a new workflow instance

**Request Properties**:

* `DefinitionId`: The workflow definition ID
* `VersionOptions`: Options for selecting the workflow version (latest, specific version, etc.)
* `CorrelationId`: Optional correlation ID for tracking related workflows
* `Input`: Dictionary of input parameters
* `InstanceId`: Optional predefined instance ID
* `TriggerActivityId`: Optional ID of a specific trigger activity to start from
* `Properties`: Additional metadata for the workflow instance

**Event Flow**:

```
1. Client calls DispatchAsync(DispatchWorkflowDefinitionRequest)
   ↓
2. Dispatcher validates the definition exists and is published
   ↓
3. Dispatcher creates a new workflow instance with the provided inputs
   ↓
4. Dispatcher enqueues the workflow for execution
   ↓
5. Background worker/executor picks up the request
   ↓
6. Workflow execution begins
   ↓
7. Activities execute in sequence/parallel based on workflow definition
   ↓
8. Workflow state is persisted (if persistence is enabled)
   ↓
9. Workflow completes, suspends (on bookmark), or faults
   ↓
10. Response returned with workflow state
```

**Example**:

```csharp
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Messages;
using Elsa.Workflows.Models;
using Microsoft.Extensions.DependencyInjection;

var dispatcher = serviceProvider.GetRequiredService<IWorkflowDispatcher>();

var request = new DispatchWorkflowDefinitionRequest
{
    DefinitionId = "order-processing-workflow",
    VersionOptions = VersionOptions.Latest,
    CorrelationId = $"order-{orderId}",
    Input = new Dictionary<string, object>
    {
        ["OrderId"] = orderId,
        ["CustomerId"] = customerId,
        ["Amount"] = orderAmount
    }
};

var response = await dispatcher.DispatchAsync(request);
Console.WriteLine($"Workflow instance created: {response.WorkflowInstanceId}");
```

### 2. DispatchWorkflowInstanceRequest

**Purpose**: Resume or continue execution of an existing workflow instance.

**Use Cases**:

* Resuming a suspended workflow that was persisted
* Re-executing a workflow that faulted
* Dispatching a loaded workflow instance for execution

**Request Properties**:

* `InstanceId`: The ID of the workflow instance to dispatch
* `Input`: Optional input to provide to the workflow on resume
* `BookmarkId`: Optional bookmark ID if resuming from a specific bookmark
* `ActivityId`: Optional activity ID to resume from
* `ActivityNodeId`: Optional activity node ID in the workflow graph

**Event Flow**:

```
1. Client calls DispatchAsync(DispatchWorkflowInstanceRequest)
   ↓
2. Dispatcher loads the workflow instance from persistence
   ↓
3. Dispatcher validates the instance exists and is in a resumable state
   ↓
4. Dispatcher enqueues the instance for execution/resumption
   ↓
5. Background worker picks up the request
   ↓
6. Workflow execution resumes from the point of suspension or specified activity
   ↓
7. Activities execute, state is persisted
   ↓
8. Workflow completes, suspends, or faults
   ↓
9. Response returned with updated workflow state
```

**Example**:

```csharp
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Messages;

var dispatcher = serviceProvider.GetRequiredService<IWorkflowDispatcher>();

var request = new DispatchWorkflowInstanceRequest
{
    InstanceId = workflowInstanceId,
    Input = new Dictionary<string, object>
    {
        ["ApprovalDecision"] = "Approved",
        ["ApprovedBy"] = userId
    }
};

var response = await dispatcher.DispatchAsync(request);
Console.WriteLine($"Workflow resumed: {response.WorkflowInstanceId}");
```

### 3. DispatchTriggerWorkflowsRequest

**Purpose**: Trigger workflows based on an external stimulus (event, HTTP request, message, etc.).

**Use Cases**:

* HTTP endpoints triggering workflows
* Message broker events (RabbitMQ, Azure Service Bus)
* Timer/scheduled triggers
* Custom event sources

**Request Properties**:

* `ActivityTypeName`: The type of trigger activity
* `BookmarkPayload`: Payload data for bookmark matching
* `CorrelationId`: Optional correlation ID
* `WorkflowInstanceId`: Optional specific instance to trigger
* `Input`: Input data for triggered workflows

**Event Flow**:

```
1. External event occurs (HTTP request, message, timer fires)
   ↓
2. Trigger handler calls DispatchAsync(DispatchTriggerWorkflowsRequest)
   ↓
3. Dispatcher queries for workflow definitions with matching triggers
   ↓
4. Dispatcher filters by trigger type and payload hash
   ↓
5. For each matching workflow definition:
   a. Create new workflow instance
   b. Enqueue for execution
   ↓
6. Background workers pick up instances
   ↓
7. Workflows execute from the trigger activity
   ↓
8. State persisted
   ↓
9. Response includes list of triggered workflow instances
```

**Example**:

```csharp
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Messages;

var dispatcher = serviceProvider.GetRequiredService<IWorkflowDispatcher>();

// Example: Triggering workflows with HTTP endpoint trigger
var request = new DispatchTriggerWorkflowsRequest
{
    ActivityTypeName = "Elsa.HttpEndpoint",
    BookmarkPayload = new
    {
        Path = "/api/webhooks/order-created",
        Method = "POST"
    },
    Input = new Dictionary<string, object>
    {
        ["RequestBody"] = requestBody,
        ["Headers"] = headers
    }
};

var response = await dispatcher.DispatchAsync(request);
Console.WriteLine($"Triggered {response.WorkflowInstanceIds.Count} workflow(s)");
```

### 4. DispatchResumeWorkflowsRequest

**Purpose**: Resume workflows that are suspended at a bookmark (waiting for an event).

**Use Cases**:

* Resuming workflows waiting for user approval
* Continuing workflows after receiving a callback
* Processing events for suspended workflows
* Timer-based resumption of delayed workflows

**Request Properties**:

* `ActivityTypeName`: Type of activity that created the bookmark
* `BookmarkPayload`: Payload for matching the bookmark
* `CorrelationId`: Optional correlation ID
* `WorkflowInstanceId`: Optional specific instance to resume
* `Input`: Input data to provide on resume

**Event Flow**:

```
1. External event occurs (approval received, callback, timer)
   ↓
2. Event handler calls DispatchAsync(DispatchResumeWorkflowsRequest)
   ↓
3. Dispatcher queries for bookmarks matching:
   - Activity type
   - Payload hash
   - Optional correlation ID or instance ID
   ↓
4. For each matching bookmark:
   a. Load the suspended workflow instance
   b. Validate instance is suspended and bookmark exists
   c. Enqueue for resumption
   ↓
5. Background workers pick up instances
   ↓
6. Workflows resume from the bookmarked activity
   ↓
7. Bookmark is "burned" (deleted) if AutoBurn is true
   ↓
8. Workflow continues execution, state persisted
   ↓
9. Response includes list of resumed workflow instances
```

**Example**:

```csharp
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Messages;

var dispatcher = serviceProvider.GetRequiredService<IWorkflowDispatcher>();

// Example: Resuming workflows waiting for approval
var request = new DispatchResumeWorkflowsRequest
{
    ActivityTypeName = "MyApp.WaitForApproval",
    BookmarkPayload = new
    {
        ApprovalId = approvalId
    },
    Input = new Dictionary<string, object>
    {
        ["Decision"] = "Approved",
        ["ApprovedBy"] = userId,
        ["ApprovedAt"] = DateTime.UtcNow
    }
};

var response = await dispatcher.DispatchAsync(request);
Console.WriteLine($"Resumed {response.WorkflowInstanceIds.Count} workflow(s)");
```

## Event Ordering and Execution Flow

Understanding the order of events during workflow dispatch is crucial for debugging and implementing custom dispatchers.

### Starting a New Workflow (DispatchWorkflowDefinitionRequest)

**Detailed Sequence**:

1. **Validate Definition**: Check that the workflow definition exists and is published
2. **Create Instance**: Instantiate a new `WorkflowInstance` with unique ID
3. **Set Input**: Apply input parameters to the workflow execution context
4. **Set Correlation**: Apply correlation ID if provided
5. **Enqueue**: Add the dispatch request to the execution queue
6. **Dequeue** (by worker): Background worker picks up the request
7. **Load Workflow**: Materialize the workflow definition into an executable graph
8. **Initialize Context**: Create workflow execution context with variables and state
9. **Execute**: Begin execution from the root activity or specified trigger
10. **Persist State**: Save workflow state after each activity or at suspension points
11. **Complete/Suspend/Fault**: Workflow reaches a terminal state
12. **Return Response**: Response includes instance ID and final/current state

### Resuming an Existing Workflow (DispatchWorkflowInstanceRequest)

**Detailed Sequence**:

1. **Validate Instance**: Check that the instance exists and is resumable
2. **Load State**: Retrieve persisted workflow state from storage
3. **Apply Input**: Merge any new input with existing workflow state
4. **Enqueue**: Add the resume request to the execution queue
5. **Dequeue** (by worker): Background worker picks up the request
6. **Reconstruct Context**: Rebuild the workflow execution context from persisted state
7. **Resume Execution**: Continue from the point of suspension or specified activity
8. **Persist State**: Save updated state after each activity
9. **Complete/Suspend/Fault**: Workflow reaches next state transition
10. **Return Response**: Response includes updated workflow state

### Triggering Workflows (DispatchTriggerWorkflowsRequest)

**Detailed Sequence**:

1. **Query Triggers**: Find all workflow definitions with matching trigger activities
2. **Filter by Type**: Match activity type (e.g., HttpEndpoint, TimerTrigger)
3. **Filter by Payload**: Match bookmark payload hash
4. **Create Instances**: For each matching definition, create a new instance
5. **Set Correlation**: Apply correlation ID from the trigger
6. **Batch Enqueue**: Add all triggered instances to the execution queue
7. **Dequeue** (by workers): Workers pick up and execute each instance
8. **Execute from Trigger**: Each workflow starts from the trigger activity
9. **Persist State**: State saved for each instance
10. **Return Response**: Response includes all triggered instance IDs

### Resuming on Bookmark (DispatchResumeWorkflowsRequest)

**Detailed Sequence**:

1. **Query Bookmarks**: Find all bookmarks matching the criteria:
   * Activity type name
   * Payload hash
   * Optional correlation ID or instance ID
2. **Acquire Locks**: For each bookmark, acquire distributed lock on the instance
3. **Validate State**: Ensure instance is still suspended and bookmark hasn't been burned
4. **Load Instances**: Load persisted state for each matching instance
5. **Batch Enqueue**: Add all resume requests to the execution queue
6. **Dequeue** (by workers): Workers pick up each resume request
7. **Resume from Bookmark**: Execution continues from the bookmarked activity
8. **Burn Bookmark**: Delete the bookmark if AutoBurn is enabled
9. **Execute Activities**: Continue through the workflow
10. **Persist State**: Save updated state
11. **Return Response**: Response includes all resumed instance IDs

## Custom Dispatcher Implementations

### Why Implement a Custom Dispatcher?

The default dispatcher (`DefaultWorkflowDispatcher`) executes workflows immediately in the same process. Custom dispatchers enable:

* **Background Processing**: Queue workflows to a message broker (RabbitMQ, Azure Service Bus, Kafka)
* **Distributed Execution**: Send workflows to specific worker nodes based on criteria (load balancing, affinity)
* **Priority Queuing**: Execute high-priority workflows first
* **Rate Limiting**: Throttle workflow execution to prevent overload
* **Custom Routing**: Route workflows to specialized workers (e.g., CPU-intensive vs I/O-bound)

### Implementing a Custom Dispatcher

```csharp
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Messages;
using System.Text.Json;

// NOTE: This example uses placeholder types (IMessageQueue and WorkflowDispatchMessage)
// for demonstration purposes. Replace these with your actual message queue infrastructure:
// - For RabbitMQ: Use MassTransit.IBus or RabbitMQ.Client
// - For Azure Service Bus: Use Azure.Messaging.ServiceBus.ServiceBusClient
// - For AWS SQS: Use Amazon.SQS.IAmazonSQS
// - For Kafka: Use Confluent.Kafka.IProducer
public class QueueBasedWorkflowDispatcher : IWorkflowDispatcher
{
    private readonly IMessageQueue _messageQueue;
    private readonly ILogger<QueueBasedWorkflowDispatcher> _logger;

    public QueueBasedWorkflowDispatcher(
        IMessageQueue messageQueue,
        ILogger<QueueBasedWorkflowDispatcher> logger)
    {
        _messageQueue = messageQueue;
        _logger = logger;
    }

    public async Task<DispatchWorkflowDefinitionResponse> DispatchAsync(
        DispatchWorkflowDefinitionRequest request,
        CancellationToken cancellationToken = default)
    {
        _logger.LogInformation(
            "Queuing workflow definition {DefinitionId} for execution",
            request.DefinitionId);

        // Generate instance ID
        var instanceId = Guid.NewGuid().ToString();

        // Serialize the request and enqueue
        var message = new WorkflowDispatchMessage
        {
            InstanceId = instanceId,
            RequestType = "StartDefinition",
            Payload = JsonSerializer.Serialize(request)
        };

        await _messageQueue.EnqueueAsync("workflow-execution-queue", message, cancellationToken);

        return new DispatchWorkflowDefinitionResponse
        {
            WorkflowInstanceId = instanceId,
            Status = WorkflowStatus.Pending
        };
    }

    public async Task<DispatchWorkflowInstanceResponse> DispatchAsync(
        DispatchWorkflowInstanceRequest request,
        CancellationToken cancellationToken = default)
    {
        _logger.LogInformation(
            "Queuing workflow instance {InstanceId} for resumption",
            request.InstanceId);

        var message = new WorkflowDispatchMessage
        {
            InstanceId = request.InstanceId,
            RequestType = "ResumeInstance",
            Payload = JsonSerializer.Serialize(request)
        };

        await _messageQueue.EnqueueAsync("workflow-execution-queue", message, cancellationToken);

        return new DispatchWorkflowInstanceResponse
        {
            WorkflowInstanceId = request.InstanceId
        };
    }

    public async Task<DispatchTriggerWorkflowsResponse> DispatchAsync(
        DispatchTriggerWorkflowsRequest request,
        CancellationToken cancellationToken = default)
    {
        _logger.LogInformation(
            "Queuing trigger request for activity type {ActivityType}",
            request.ActivityTypeName);

        // Query for matching workflow definitions (implementation depends on your store)
        var matchingDefinitions = await FindMatchingTriggersAsync(request, cancellationToken);

        var instanceIds = new List<string>();

        foreach (var definition in matchingDefinitions)
        {
            var instanceId = Guid.NewGuid().ToString();
            instanceIds.Add(instanceId);

            var message = new WorkflowDispatchMessage
            {
                InstanceId = instanceId,
                RequestType = "Trigger",
                Payload = JsonSerializer.Serialize(new
                {
                    DefinitionId = definition.DefinitionId,
                    TriggerRequest = request
                })
            };

            await _messageQueue.EnqueueAsync("workflow-execution-queue", message, cancellationToken);
        }

        return new DispatchTriggerWorkflowsResponse
        {
            WorkflowInstanceIds = instanceIds
        };
    }

    public async Task<DispatchResumeWorkflowsResponse> DispatchAsync(
        DispatchResumeWorkflowsRequest request,
        CancellationToken cancellationToken = default)
    {
        _logger.LogInformation(
            "Queuing resume request for activity type {ActivityType}",
            request.ActivityTypeName);

        // Query for matching bookmarks (implementation depends on your store)
        var matchingBookmarks = await FindMatchingBookmarksAsync(request, cancellationToken);

        var instanceIds = new List<string>();

        foreach (var bookmark in matchingBookmarks)
        {
            instanceIds.Add(bookmark.WorkflowInstanceId);

            var message = new WorkflowDispatchMessage
            {
                InstanceId = bookmark.WorkflowInstanceId,
                RequestType = "Resume",
                Payload = JsonSerializer.Serialize(new
                {
                    BookmarkId = bookmark.Id,
                    ResumeRequest = request
                })
            };

            await _messageQueue.EnqueueAsync("workflow-execution-queue", message, cancellationToken);
        }

        return new DispatchResumeWorkflowsResponse
        {
            WorkflowInstanceIds = instanceIds
        };
    }

    // NOTE: The following methods are intentionally incomplete example code.
    // They demonstrate the pattern for querying workflow definitions and bookmarks
    // but should be implemented based on your specific storage configuration.
    
    private async Task<List<WorkflowDefinition>> FindMatchingTriggersAsync(
        DispatchTriggerWorkflowsRequest request,
        CancellationToken cancellationToken)
    {
        // Query your workflow definition store for definitions with triggers matching the request.
        // Recommended implementation using Elsa's built-in services:
        // 
        // 1. Inject IWorkflowDefinitionStore from Elsa.Workflows.Management namespace
        // 2. Use FindManyAsync with a filter:
        //    - IsPublished = true
        //    - Filter by definitions containing trigger activities matching request.ActivityTypeName
        // 3. For each definition, check if trigger payload hash matches request.BookmarkPayload
        // 4. Return list of matching WorkflowDefinition objects
        //
        // Example:
        // var filter = new WorkflowDefinitionFilter { IsPublished = true };
        // var definitions = await _workflowDefinitionStore.FindManyAsync(filter, cancellationToken);
        // return definitions.Where(def => HasMatchingTrigger(def, request)).ToList();
        
        throw new NotImplementedException("Implement using IWorkflowDefinitionStore from Elsa.Workflows.Management");
    }

    private async Task<List<Bookmark>> FindMatchingBookmarksAsync(
        DispatchResumeWorkflowsRequest request,
        CancellationToken cancellationToken)
    {
        // Query your bookmark store for bookmarks matching the request.
        // Recommended implementation using Elsa's built-in services:
        //
        // 1. Inject IBookmarkStore from Elsa.Workflows.Runtime namespace
        // 2. Use FindManyAsync with a BookmarkFilter:
        //    - ActivityTypeName = request.ActivityTypeName
        //    - Hash = compute hash from request.BookmarkPayload
        //    - Optionally: CorrelationId, WorkflowInstanceId
        // 3. Return list of matching Bookmark objects
        //
        // Example:
        // var filter = new BookmarkFilter
        // {
        //     ActivityTypeName = request.ActivityTypeName,
        //     Hash = _hasher.Hash(request.BookmarkPayload),
        //     CorrelationId = request.CorrelationId,
        //     WorkflowInstanceId = request.WorkflowInstanceId
        // };
        // return await _bookmarkStore.FindManyAsync(filter, cancellationToken);
        
        throw new NotImplementedException("Implement using IBookmarkStore from Elsa.Workflows.Runtime");
    }
}
```

### Registering a Custom Dispatcher

```csharp
using Microsoft.Extensions.DependencyInjection;

builder.Services.AddElsa(elsa =>
{
    // Replace the default dispatcher with your custom implementation
    elsa.Services.AddSingleton<IWorkflowDispatcher, QueueBasedWorkflowDispatcher>();
});
```

## Multi-Process and Multi-Node Considerations

When running Elsa in a distributed environment (multiple nodes/processes), understanding dispatcher behavior is critical:

### Distributed Locking

* The dispatcher itself doesn't implement locking
* Locking happens at the **execution** level via `IDistributedLockProvider`
* When resuming workflows, ensure distributed locks prevent concurrent execution of the same instance

### Bookmark Resolution

* Bookmarks are stored in a shared database
* Multiple nodes can query bookmarks simultaneously
* The first node to acquire the lock on an instance wins
* Bookmark hashing must be deterministic across all nodes

### Queue-Based Dispatch

For true distributed execution:

1. Dispatcher enqueues to a message broker
2. Worker nodes consume from the queue
3. Workers execute workflows using `IWorkflowRunner`
4. State is persisted to shared storage
5. Workers release locks after execution

### Singleton Scheduler

For timer/scheduled workflows in clusters:

* Use Quartz clustering to ensure only one node schedules timers
* Or designate a single "scheduler" node
* See [Clustering Guide](/guides/clustering) for configuration

## Troubleshooting Dispatcher Issues

### Workflows Not Starting

**Symptoms**: Dispatch calls succeed but workflows don't execute

**Checks**:

1. Verify the dispatcher is properly registered
2. Check for background worker or queue consumer running
3. Verify workflow definition is published
4. Check logs for exceptions during dispatch or execution

### Duplicate Executions

**Symptoms**: Same workflow executes multiple times from a single trigger

**Causes**:

* Multiple nodes dispatching the same trigger without coordination
* Missing distributed locks during resume
* Bookmark not burned after first use

**Solutions**:

* Implement distributed locking
* Set `AutoBurn = true` on bookmarks
* Use idempotent activities

### Bookmarks Not Matching

**Symptoms**: Resume requests don't find bookmarks

**Causes**:

* Payload structure mismatch between create and resume
* Hash computed differently on different nodes
* Case sensitivity in payload properties

**Solutions**:

* Use shared payload classes/records
* Ensure consistent serialization settings
* Log and compare payload hashes

## Related Documentation

* [Running Workflows](/guides/running-workflows) - High-level guide to workflow execution
* [Clustering Guide](/guides/clustering) - Multi-node deployment
* [Distributed Hosting](/hosting/distributed-hosting) - Distributed architecture patterns
* [Blocking Activities & Triggers](/activities/blocking-and-triggers) - Bookmark fundamentals
* [Troubleshooting Guide](/guides/troubleshooting) - Debugging workflows

## Summary

The `IWorkflowDispatcher` is the core dispatching abstraction in Elsa Workflows:

* **Four dispatch types**: Start definition, resume instance, trigger workflows, resume bookmarks
* **Event-driven**: Enables decoupled, asynchronous workflow execution
* **Customizable**: Implement custom dispatchers for background processing, queuing, and distributed scenarios
* **Orchestrates execution**: Manages the flow from dispatch to enqueue to execution
* **Foundation for triggers**: All triggers use the dispatcher to start/resume workflows

Understanding the dispatcher's role and event ordering helps you:

* Design robust distributed workflow systems
* Troubleshoot execution issues
* Implement custom execution strategies
* Optimize workflow performance

For most applications, the default dispatcher works well. Consider custom implementations when you need:

* Background/queued processing
* Distributed execution across nodes
* Custom routing or load balancing
* Integration with existing message brokers


# Workflow Dispatch Outbox

Release-backed guidance for configuring, operating, and diagnosing Elsa's transactional outbox for workflow dispatch.

Use the workflow dispatch outbox when a workflow dispatch must not become visible until the current workflow state has been committed. It coordinates in-workflow dispatch with the owner's state commit and provides recovery when delivery is delayed or the host restarts.

The outbox is for Elsa workflow dispatch commands. It is not a general-purpose outbox for arbitrary application messages or external side effects.

{% hint style="warning" %}
"Transactional" describes marker-gated coordination: the outbox item is written before the workflow state commit, and the processor delivers it only after the committed owner state contains its marker. This is not one atomic transaction spanning the outbox store and the workflow-state store.
{% endhint %}

## When to use it

Enable it when a workflow starts, resumes, or triggers other workflows and the child or resumed work must not be dispatched from a parent state that may later be rolled back. This is especially useful for long-running, distributed, or failure-sensitive workflows.

The outbox does not change dispatch calls made outside workflow execution. In that case, Elsa uses the normal background dispatcher. It also does not make the downstream workflow's activities transactional with the parent workflow.

## Enable the outbox

Configure `WorkflowDispatcherOptions` in the Elsa host:

```csharp
using Elsa.Workflows.Runtime.Options;

builder.Services.Configure<WorkflowDispatcherOptions>(options =>
{
    options.UseTransactionalOutbox = true;
    options.ProcessOutboxAfterCommit = true;
    options.OutboxProcessorBatchSize = 100;
});
```

The Elsa runtime registers the outbox, its processor, the post-commit handler, and a recurring processor when the workflow runtime feature is enabled. The default execution pipeline also includes the middleware that exposes the current workflow execution context to the outbox. If you replace that pipeline, retain `UseWorkflowDispatchOutbox()`.

The release defaults are:

| Option                        | Default | Effect                            |
| ----------------------------- | ------: | --------------------------------- |
| `UseTransactionalOutbox`      | `false` | Enables transactional dispatch.   |
| `ProcessOutboxAfterCommit`    |  `true` | Attempts delivery after a commit. |
| `OrphanedOutboxItemRetention` |   1 day | Keeps ownerless items.            |
| `MaxOutboxDeliveryAttempts`   |      10 | Failed sends before abandonment.  |
| `OutboxProcessorBatchSize`    |     100 | Items loaded per processor cycle. |

`ProcessOutboxAfterCommit = false` does not disable the outbox. It skips the eager post-commit attempt and leaves delivery to the recurring sweep.

## How delivery works

For a dispatch made during workflow execution, Elsa follows this sequence:

1. The transactional dispatcher creates an outbox item for a definition, instance, trigger, or resume dispatch.
2. Elsa stores the item and adds its ID to the current workflow state as an ownership marker.
3. The workflow state commit persists that marker with the owner workflow.
4. After the commit, Elsa may try to process the item immediately. A recurring task also scans for pending items every 10 seconds by default.
5. The processor takes a distributed lock, verifies that the owner exists and that its committed state contains the item ID, and sends the command through the background command path.
6. After a successful send, Elsa deletes the outbox item and removes its marker from the owner workflow state.

The processor uses a tenant-scoped lock when a tenant is active, and preserves the item's tenant ID as dispatch headers. Multiple nodes can therefore run the processor without intentionally sending the same pending item concurrently, provided they use a shared distributed lock provider. The release default is a file-system lock under `App_Data/locks`, which is suitable for one machine but must be replaced for nodes running on separate machines. The lock does not provide exactly-once delivery.

## What is placed in the outbox

The transactional dispatcher supports these four command kinds:

| Command kind        | Meaning                                             |
| ------------------- | --------------------------------------------------- |
| Workflow definition | Start a new workflow instance from a definition.    |
| Workflow instance   | Dispatch an existing workflow instance.             |
| Trigger workflows   | Dispatch workflows matched by a stimulus.           |
| Resume workflows    | Resume workflows matched by a stimulus or bookmark. |

The **Dispatch Workflow** and **Bulk Dispatch Workflows** activities use `IWorkflowDispatcher` while they execute, so their child dispatches can enter the outbox. If either activity is configured to wait for child completion, the parent still waits on its bookmark; the outbox only controls when the child dispatch becomes eligible for delivery.

## Failure, retry, and cleanup behavior

### Delivery failures

If sending the command fails, Elsa increments `DeliveryAttempts` and keeps the item for a later processor cycle. There is no application-level exponential backoff in this processor; the retry cadence is determined by the eager attempt and recurring sweep. The processor continues with the next item in the batch when one item fails.

When the attempt count reaches `MaxOutboxDeliveryAttempts`, Elsa abandons the item by deleting it and removing its committed marker. If deletion fails, Elsa preserves the attempt count and tries again during a later cycle.

### Missing or uncommitted owners

An item is not delivered until its owner workflow exists and its committed state contains the item ID. This prevents an item saved before a failed or rolled-back owner commit from being dispatched.

If the owner is missing, or the owner never committed the marker, Elsa retains the item until `OrphanedOutboxItemRetention` expires. Set that option to zero or less only when immediate cleanup of such items is acceptable.

### At-least-once delivery

Delivery is at-least-once. If Elsa sends the command successfully but cannot delete the item, the item is not counted as a delivery failure and may be sent again. The outbox processor does not deduplicate child starts; the owner marker and outbox item ID only guard commit eligibility. Use application-level idempotency or deduplication where duplicate delivery would be harmful.

If the item is deleted successfully but marker cleanup fails, Elsa does not recreate the item. The owner can temporarily retain a stale marker, which a later commit or state sweep may prune.

Retry limits and orphan cleanup intentionally delete or abandon items. The release does not provide a separate dead-letter store or replay endpoint, so retain the relevant logs and failure context if operators need to investigate an abandoned dispatch.

The default key-value outbox store also includes recovery and index records so an interrupted store write can be found and repaired on a later scan. For production, ensure that the configured key-value store and workflow-instance store are durable and shared by the nodes that process the same tenant/workload. Without a persistence provider, the release's default key-value store is in-memory and is not a restart-safe outbox.

## Operating and diagnosing the outbox

Use this checklist when a child workflow is delayed or appears more than once:

* **Dispatch is delayed after a commit:** Check `ProcessOutboxAfterCommit`, the 10-second sweep, batch size, processor lock, and command-worker backlog.
* **The item remains after a send failure:** Inspect the delivery-failure log and `DeliveryAttempts`; confirm the configured maximum has not been reached.
* **The item is never sent:** Confirm the owner workflow committed successfully and that the workflow-state store and outbox store are available from the same host.
* **The same child appears more than once:** Treat this as possible redelivery under at-least-once semantics; inspect outbox deletion failures and make the downstream operation idempotent.
* **Items disappear without delivery:** Check orphan retention and the `Abandoning workflow dispatch outbox item` warning. Both missing owners and max-attempt items are intentionally cleaned up.
* **Only one node appears to process the queue:** This is expected while the distributed processor lock is held. Check lock-provider configuration and tenant identity when progress stops. A local file lock is not sufficient for nodes on separate machines.

For Studio users, the outbox is server-side runtime behavior. Configure it in the host, then use the activity's **Wait for Completion** option according to the workflow contract. The option controls whether the parent waits for the child; it does not turn delivery into a transaction across both workflows. Core exposes no outbox inspection endpoint, so operational diagnosis relies on host logs, persistence-store telemetry, and workflow-state investigation.

## Related guides

* [Workflow Dispatcher Architecture](/guides/architecture/workflow-dispatcher) explains the dispatcher contracts and request types.
* [Dispatch Workflow Activity](/guides/running-workflows/dispatch-workflow-activity) explains how to start one child workflow from a workflow.
* [Bulk Dispatch Workflows Activity](/guides/running-workflows/bulk-dispatch-workflows) explains fan-out dispatch and completion behavior.
* [Performance tuning](/guides/performance) covers measurement-driven batch and commit-path tuning.
* [Distributed hosting](/hosting/distributed-hosting) covers the multi-node locking and shared-storage prerequisites.

## Release source references

This guide is grounded in Elsa Core `release/3.8.0` at [`e96c8f23`](https://github.com/elsa-workflows/elsa-core/tree/e96c8f23c998ee01d1b63151d26832b31be534b):

* [`WorkflowDispatcherOptions`](https://github.com/elsa-workflows/elsa-core/blob/e96c8f23c998ee01d1b63151d26832b31be534b/src/modules/Elsa.Workflows.Runtime/Options/WorkflowDispatcherOptions.cs)
* [`TransactionalWorkflowDispatcher`](https://github.com/elsa-workflows/elsa-core/blob/e96c8f23c998ee01d1b63151d26832b31be534b/src/modules/Elsa.Workflows.Runtime/Services/TransactionalWorkflowDispatcher.cs)
* [`WorkflowDispatchOutboxProcessor`](https://github.com/elsa-workflows/elsa-core/blob/e96c8f23c998ee01d1b63151d26832b31be534b/src/modules/Elsa.Workflows.Runtime/Services/WorkflowDispatchOutboxProcessor.cs)
* [`ProcessWorkflowDispatchOutbox`](https://github.com/elsa-workflows/elsa-core/blob/e96c8f23c998ee01d1b63151d26832b31be534b/src/modules/Elsa.Workflows.Runtime/Handlers/ProcessWorkflowDispatchOutbox.cs)
* [`WorkflowRuntimeFeature`](https://github.com/elsa-workflows/elsa-core/blob/e96c8f23c998ee01d1b63151d26832b31be534b/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs)
* [`KeyValueFeature`](https://github.com/elsa-workflows/elsa-core/blob/e96c8f23c998ee01d1b63151d26832b31be534b/src/modules/Elsa.KeyValues/ShellFeatures/KeyValueFeature.cs)
* [`WorkflowDispatchCommandFactory`](https://github.com/elsa-workflows/elsa-core/blob/e96c8f23c998ee01d1b63151d26832b31be534b/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowDispatchCommandFactory.cs)


# Runtime Coordination Storage

Configure the key-value and distributed-lock providers that Elsa uses for durable outbox records and multi-node workflow coordination.

Elsa has two infrastructure contracts that are easy to confuse:

| Contract                   | What it stores or coordinates                                                                           | Elsa 3.8.0 default                 |
| -------------------------- | ------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| `IKeyValueStore`           | Small serialized records such as outbox items, instance heartbeats, and persisted administrative pauses | In-memory                          |
| `IDistributedLockProvider` | Leases that ensure only one node performs a coordinated operation at a time                             | Local files under `App_Data/locks` |

These defaults are useful for development and single-host tests. They are not durable or cross-node production infrastructure. A production cluster needs a key-value provider visible to every node and a lock provider that coordinates across every node.

{% hint style="warning" %}
`UseDistributedRuntime()` does not select a shared lock provider or make the default key-value store durable. Configure both storage boundaries explicitly when more than one host can process the same Elsa deployment.
{% endhint %}

## What uses each contract?

The stores are infrastructure, not user-facing workflow data stores. Their consumers have different failure and durability requirements:

| Runtime path                             | Contract                                        | Operational consequence                                                                                                                                                                             |
| ---------------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Workflow dispatch outbox                 | `IKeyValueStore` and `IDistributedLockProvider` | Pending dispatch records must survive restarts, and only one processor should claim a tenant-scoped batch at a time. See [Workflow dispatch outbox](/guides/architecture/workflow-dispatch-outbox). |
| Distributed bookmark queue worker        | `IDistributedLockProvider`                      | Only one node processes the distributed queue at a time; a node that cannot acquire the lock retries locally.                                                                                       |
| Workflow resumption and trigger indexing | `IDistributedLockProvider`                      | Concurrent resume and trigger-indexing operations are serialized across nodes.                                                                                                                      |
| Instance heartbeats                      | `IKeyValueStore` and `IDistributedLockProvider` | Every node writes a heartbeat; the monitor takes a lock before finding timed-out nodes.                                                                                                             |
| Administrative pause persistence         | `IKeyValueStore`                                | `PausePersistencePolicy.AcrossReactivations` can restore a pause after the host is reactivated.                                                                                                     |

The key-value store is not a replacement for the workflow definition, instance, bookmark, or execution-log stores. Configure the runtime persistence provider for those stores as well, and point every node at the same logical runtime database or document store.

## Choose a key-value provider

The default `MemoryKeyValueStore` loses its records when the process exits and is local to one process. Select a runtime persistence provider that also registers an `IKeyValueStore` implementation:

| Runtime persistence | Key-value implementation in the release line | Typical fit                                                                  |
| ------------------- | -------------------------------------------- | ---------------------------------------------------------------------------- |
| EF Core             | `EFCoreKeyValueStore`                        | Relational database deployments that already use EF Core runtime persistence |
| MongoDB             | `MongoKeyValueStore`                         | Deployments using the MongoDB runtime provider                               |
| Dapper              | `DapperKeyValueStore`                        | Deployments using the Dapper runtime provider                                |

For EF Core, the runtime persistence feature wires the key-value store for you:

```csharp
using Elsa.Extensions;
using Elsa.Persistence.EFCore.Modules.Management;
using Elsa.Persistence.EFCore.Modules.Runtime;

builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowManagement(management =>
    {
        management.UseEntityFrameworkCore(ef =>
        {
            ef.UsePostgreSql(builder.Configuration.GetConnectionString("Elsa"));
        });
    });

    elsa.UseWorkflowRuntime(runtime =>
    {
        runtime.UseEntityFrameworkCore(ef =>
        {
            ef.UsePostgreSql(builder.Configuration.GetConnectionString("Elsa"));
        });
    });
});
```

Use the matching runtime persistence extension when you use MongoDB or Dapper. Do not register a durable workflow instance store while leaving `IKeyValueStore` on the in-memory default: outbox records and coordination records would still disappear on restart.

{% hint style="info" %}
The outbox is marker-gated rather than one atomic transaction across every store. A durable key-value provider improves recovery, but it does not turn a workflow-state commit and an outbox write into a single cross-store transaction. See [Workflow dispatch outbox](/guides/architecture/workflow-dispatch-outbox) for the delivery and recovery semantics.
{% endhint %}

## Choose a distributed lock provider

The runtime feature exposes a provider factory on `UseWorkflowRuntime`. The release default is a `FileDistributedSynchronizationProvider` rooted at `App_Data/locks`. That coordinates processes that can safely share the same local file system; it does not coordinate independent containers or hosts.

Configure a provider supplied by your infrastructure package. Redis, PostgreSQL, and SQL Server are examples of cross-node providers supported by the release's startup diagnostic. The provider must use the same shared resource namespace and compatible lease/timeout settings on every node.

The following shows the Elsa integration point; the provider construction is intentionally omitted because its type and connection lifecycle depend on the package you choose:

```csharp
using Elsa.Extensions;
using Elsa.Workflows.Runtime.Distributed.Extensions;
using Medallion.Threading;

// Construct this with the provider package and shared connection used by every node.
// The constructor is provider-specific; see the Redis example below.
IDistributedLockProvider crossNodeLockProvider = CreateCrossNodeLockProvider();

builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowRuntime(runtime =>
    {
        runtime.UseDistributedRuntime();
        runtime.DistributedLockProvider = _ => crossNodeLockProvider;
        runtime.DistributedLockingOptions = options =>
        {
            options.LockAcquisitionTimeout = TimeSpan.FromMinutes(2);
        };
    });
});
```

Use the constructor and connection lifecycle recommended by the Redis, PostgreSQL, SQL Server, or cloud-lock package you selected. The important Elsa integration point is `runtime.DistributedLockProvider`; registering an unrelated provider under a different service key does not replace this feature setting.

For a complete Redis example, see [Redis distributed locking](https://github.com/elsa-workflows/elsa-gitbook/tree/main/guides/clustering/examples/redis-lock-setup.md). For the broader distributed-hosting checklist, see [Distributed hosting](/hosting/distributed-hosting).

## Development and single-host exceptions

The distributed runtime validates its provider during startup. If it detects the built-in file or no-op provider, it logs a warning because the provider cannot coordinate across application nodes. The warning is useful: do not silence it as a substitute for configuring a shared provider.

For a deliberately single-host development or test deployment, acknowledge the local provider explicitly:

```csharp
elsa.UseWorkflowRuntime(runtime =>
{
    runtime.UseDistributedRuntime();
    runtime.DistributedLockingOptions = options =>
    {
        options.AllowLocalLockProviderInDistributedRuntime = true;
    };
});
```

The Elsa Server sample enables this option by default only in development. In production, set it to `false` and treat any local-provider warning as a configuration defect. A shared network folder is not a substitute for a lock service: file semantics and failure detection may not be consistent across hosts.

## Validate a deployment

Before adding a second node, verify all of the following:

1. Every node uses the same durable runtime persistence provider and logical database or document store.
2. Every node resolves the same cross-node `IDistributedLockProvider`.
3. The lock provider can acquire and release a probe lock from each node.
4. Outbox, heartbeat, and runtime coordination records remain visible after a restart.
5. A two-node test exercises concurrent resume, trigger indexing, and outbox processing; inspect logs for local-provider warnings and lock timeouts.
6. Tenant-aware workloads use the same tenant resolution and shared storage configuration on every node.

Elsa exposes a distributed-lock readiness check when you register readiness checks with `includeDistributedLocks: true`. Add it to the host's readiness pipeline so a node with an unreachable lock provider is not presented as ready:

```csharp
using Elsa.Extensions;

builder.Services
    .AddHealthChecks()
    .AddElsaReadinessChecks(includeDistributedLocks: true);
```

For the complete runtime, persistence, and endpoint contract—including the sample's `/health/live` and `/health/ready` mappings—see [Readiness and Health Checks](/operate/readiness-and-health-checks).

The readiness probe confirms that the configured provider can reach and acquire a probe lock; it does not prove that the provider is configured with the right business-level topology. Keep the two-node concurrency test in your deployment validation as well.

## Related guides

* [Workflow dispatch outbox](/guides/architecture/workflow-dispatch-outbox)
* [Distributed hosting](/hosting/distributed-hosting)
* [Redis distributed locking](https://github.com/elsa-workflows/elsa-gitbook/tree/main/guides/clustering/examples/redis-lock-setup.md)
* [Persistence](/guides/persistence)
* [Performance tuning](/guides/performance)


# Standalone and Modular Hosting

Release-backed configuration matrix for code-first Elsa hosts and the CShells-based modular server in Elsa 3.8.0.

Elsa 3.8.0 supports two host-composition models:

* **Standalone (code-first)**: register Elsa in an ASP.NET Core application with `AddElsa(...)`, then compose modules with `UseX(...)` methods.
* **Modular**: use `Elsa.ModularServer.Web`, register CShells, and activate shell features from configuration. This is useful when feature composition and per-shell settings need to be changed without recompiling the host.

These models use the same Elsa modules, but their configuration surfaces are different. Use the matrix below when moving from one host model to the other.

## Host registration

| Concern                          | Standalone host                                                                                | Modular host                                                                                                             |
| -------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Register Elsa                    | `services.AddElsa(elsa => { ... })`                                                            | `builder.AddShells(shells => shells.WithConfigurationProvider(configuration) ...)`                                       |
| Activate features                | Call extension methods such as `UseIdentity()`, `UseWorkflowRuntime()`, or `UseWorkflowsApi()` | Add shell feature types in `shell.WithFeatures(...)` and configure their settings under `CShells:Shells:<name>:Features` |
| Set feature options              | Configure the feature callback or bind an options section in C#                                | Set the shell feature's public settings under that feature's configuration object                                        |
| Add a route prefix or shell path | Configure the relevant ASP.NET Core or Elsa option in code                                     | Configure the shell's `WebRouting` settings and enable CShells web routing in the host                                   |
| Add or remove a capability       | Change the compiled `UseX(...)` chain                                                          | Change the shell feature set and reload/restart according to the modular host's lifecycle                                |

The release modular server wires these pieces together in `Elsa.ModularServer.Web/Program.cs`:

```csharp
builder.AddShells(shells => shells
    .WithHostAssemblies()
    .WithConfigurationProvider(configuration)
    .WithWebRouting(options => options.EnablePathRouting = true)
    .WithAuthenticationAndAuthorization()
    .ConfigureAllShells(shell => shell.WithFeatures(
        typeof(ElsaFeature),
        typeof(WorkflowManagementFeature),
        typeof(WorkflowRuntimeFeature),
        typeof(WorkflowsFeature),
        typeof(WorkflowsApiFeature))));
```

`ConfigureAllShells(...)` supplies the feature set to every shell created by the host. The values for an individual shell are then read from that shell's configuration section.

## Configuration matrix

The modular column separates features activated in the host's `WithFeatures(...)` list from settings exposed by a shell feature. A feature can be active without needing a configuration object.

| Capability                      | Standalone registration                                                                   | Modular feature/settings                                                                                                                                                                                                                    |
| ------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Elsa core                       | `elsa` module is created by `AddElsa(...)`                                                | `"Elsa": {}`                                                                                                                                                                                                                                |
| Identity services               | `.UseIdentity(...)`                                                                       | `"Identity": { "SigningKey": "..." }`                                                                                                                                                                                                       |
| JWT/API-key authentication      | `.UseDefaultAuthentication()`                                                             | `"DefaultAuthentication": {}`                                                                                                                                                                                                               |
| Workflow management and runtime | `.UseWorkflowManagement(...)` and `.UseWorkflowRuntime(...)`                              | Activate `WorkflowManagementFeature` and `WorkflowRuntimeFeature` in `WithFeatures(...)`; provider-specific persistence features supply storage. The shipped `appsettings.json` does not need empty settings objects for these two features |
| Workflow API                    | `.UseWorkflowsApi()`                                                                      | `"WorkflowsApi": {}`                                                                                                                                                                                                                        |
| HTTP activities and triggers    | `.UseHttp(http => { ... })`                                                               | `"Http": { "HttpActivityOptions": { ... } }`                                                                                                                                                                                                |
| SQLite workflow persistence     | `UseEntityFrameworkCore(ef => ef.UseSqlite(...))` in the management/runtime configuration | `"SqliteWorkflowPersistence": { "ConnectionString": "..." }`                                                                                                                                                                                |

For example, the standalone HTTP configuration in the release server binds `HttpActivityOptions` in the `UseHttp(...)` callback. The modular `HttpFeature` exposes the same options as its `HttpActivityOptions` setting, so the modular equivalent is:

```json
{
  "CShells": {
    "Shells": {
      "Default": {
        "Features": {
          "Http": {
            "HttpActivityOptions": {
              "BaseUrl": "https://localhost:5001"
            }
          }
        }
      }
    }
  }
}
```

For a code-first host, the equivalent is an explicit callback:

```csharp
services.AddElsa(elsa => elsa.UseHttp(http =>
{
    http.ConfigureHttpOptions = options =>
        configuration.GetSection("Http").Bind(options);
}));
```

The important distinction is the level at which the setting is bound: a standalone callback binds an Elsa options object directly, while a modular feature receives its own shell-scoped configuration object.

## Identity and secrets

The configuration path is not interchangeable between host models:

* code-first `Elsa.Server.Web` binds token settings from `Identity:Tokens`;
* the modular `Identity` shell feature binds its settings from the shell's `Identity` feature section, for example `CShells:Shells:Default:Features:Identity:SigningKey`.

For environment variables, that modular path becomes:

```
CShells__Shells__Default__Features__Identity__SigningKey
```

Do not copy a code-first `Identity:Tokens:SigningKey` environment variable into a modular host and assume it will be discovered. Bind the setting at the configuration path used by the host model.

## Choosing a model

Choose a standalone host when:

* Elsa is part of an existing ASP.NET Core application;
* feature composition is owned by the application code;
* you need arbitrary C# callbacks, custom services, or application-specific startup logic.

Choose a modular host when:

* the deployment needs CShells and shell-scoped configuration;
* feature sets are assembled from host or package assemblies;
* different shells need different settings such as route paths or persistence connections.

The modular model does not make `AddElsa(...)` configuration automatically available. A `UseX(...)` call must be translated to the corresponding shell feature, and any callback logic must be replaced by settings that the shell feature actually exposes.

When a NuGet extension includes an `elsa-package.json` manifest, use it as a metadata and packaging aid. It describes runtime compatibility, infrastructure requirements, and deploy-time settings; the shell still has to register the feature and bind its settings. See [Package manifests for extensions](/guides/plugins-modules/package-manifests) for the annotation and build contract.

## Configuration-shape warning

The 3.8.0 source tree contains both `src/apps/Elsa.ModularServer.Web/appsettings.json` and `appsettings.Example.json`. They show different shell collection shapes: the checked-in runtime file uses an object keyed by shell name, while the example file uses a list of shell objects with `Name`, `Settings`, and `Features` fields.

Treat the configuration file shipped with the exact modular host and its CShells package version as authoritative. Do not combine the two shapes in one deployment without verifying how that host's configuration provider parses them.

## Related guides

* [Hosting Elsa in an Existing App](/guides/onboarding/hosting-elsa-in-existing-app)
* [Modules and Plugins](/guides/modules-and-plugins)
* [Configuration Management](/guides/deployment/configuration-management)
* [Authentication & Authorization](/guides/authentication)

## Release source references

* [Standalone `AddElsa` registration](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/src/modules/Elsa/Extensions/DependencyInjectionExtensions.cs)
* [Standalone server composition](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/src/apps/Elsa.Server.Web/Program.cs)
* [Modular server composition](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/src/apps/Elsa.ModularServer.Web/Program.cs)
* [Modular server settings](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/src/apps/Elsa.ModularServer.Web/appsettings.json)
* [Identity shell feature](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/src/modules/Elsa.Identity/ShellFeatures/IdentityFeature.cs)
* [HTTP shell feature](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/src/modules/Elsa.Http/ShellFeatures/HttpFeature.cs)
* [Module system notes](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/doc/wiki/module-system.md)


# Onboarding

This section provides guides to help you get started with integrating Elsa Workflows into your applications.

## Guides in This Section

* [Hosting Elsa in an Existing App](/guides/onboarding/hosting-elsa-in-existing-app) - Step-by-step guide to adding Elsa to an existing ASP.NET Core application, including persistence setup and common integration challenges.

## Related Documentation

* [Getting Started](/getting-started/hello-world) - Basic tutorials and concepts
* [Application Types](/application-types/elsa-server) - Understanding different Elsa deployment options
* [Database Configuration](/getting-started/database-configuration) - Persistence setup
* [Deployment](/guides/deployment/kubernetes) - Production deployment guides


# Hosting Elsa in an Existing App

Step-by-step guide to integrating Elsa Workflows into an existing ASP.NET Core application, including persistence setup, common pain points, and troubleshooting.

This guide walks you through adding Elsa Workflows to an existing ASP.NET Core application. Whether you're building a new feature or modernizing an existing codebase, this guide will help you integrate Elsa smoothly while avoiding common pitfalls.

## Overview

Integrating Elsa into an existing ASP.NET Core app involves:

1. Installing required NuGet packages
2. Configuring Elsa services in `Program.cs`
3. Setting up persistence (database)
4. Addressing common integration challenges

This guide focuses on practical integration patterns and addresses common issues reported by the community (issue #6).

## Prerequisites

Before you begin, ensure you have:

* An existing ASP.NET Core application (.NET 6.0+, .NET 8.0 recommended)
* Basic understanding of ASP.NET Core dependency injection
* A database server (PostgreSQL, SQL Server, SQLite, or MySQL)
* Visual Studio 2022+, Visual Studio Code, or Rider

## Step 1: Install Elsa Packages

Add the core Elsa packages to your project. The exact packages depend on your needs:

### Basic Workflow Runtime

For workflow execution without a UI:

```bash
dotnet add package Elsa
dotnet add package Elsa.Workflows.Runtime
dotnet add package Elsa.Workflows.Api
```

### With Entity Framework Core Persistence

For PostgreSQL:

```bash
dotnet add package Elsa.Persistence.EFCore.PostgreSql
```

For SQL Server:

```bash
dotnet add package Elsa.Persistence.EFCore.SqlServer
```

For SQLite (development only):

```bash
dotnet add package Elsa.Persistence.EFCore.Sqlite
```

### Optional: HTTP Activities

If your workflows need HTTP endpoints:

```bash
dotnet add package Elsa.Http
```

### Optional: Elsa Studio (Web UI)

To include the workflow designer UI in your app:

```bash
dotnet add package Elsa.Studio
dotnet add package Elsa.Studio.Core.BlazorWasm
```

## Step 2: Configure Elsa in Program.cs

Add Elsa to your existing `Program.cs` configuration. Here's a complete example showing integration with an existing app:

### Basic Configuration

```csharp
using Elsa.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Your existing services
builder.Services.AddControllers();
builder.Services.AddRazorPages();
// ... other services ...

// Add Elsa services
builder.Services.AddElsa(elsa =>
{
    // Configure workflow management (designer, definitions)
    elsa.UseWorkflowManagement();
    
    // Configure workflow runtime (execution engine)
    elsa.UseWorkflowRuntime(runtime =>
    {
        runtime.WorkflowInboxCleanupOptions = new()
        {
            // Clean up completed workflow instances after 30 days
            BatchSize = 100,
            SweepInterval = TimeSpan.FromMinutes(60)
        };
    });
    
    // Expose workflows via REST API
    elsa.UseWorkflowsApi();
    
    // Add HTTP activities for workflow endpoints
    elsa.UseHttp();
});

var app = builder.Build();

// Your existing middleware
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

// Map Elsa workflows API endpoints
app.UseWorkflowsApi();

// Your existing endpoints
app.MapControllers();
app.MapRazorPages();

app.Run();
```

### With Persistence (PostgreSQL Example)

```csharp
using Elsa.Persistence.EFCore.Extensions;
using Elsa.Persistence.EFCore.Modules.Management;
using Elsa.Persistence.EFCore.Modules.Runtime;
using Elsa.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Your existing services
builder.Services.AddControllers();

// Add Elsa with PostgreSQL persistence
builder.Services.AddElsa(elsa =>
{
    // Use PostgreSQL for workflow definitions and instances
    elsa.UseWorkflowManagement(management =>
    {
        management.UseEntityFrameworkCore(ef =>
        {
            ef.UsePostgreSql(builder.Configuration.GetConnectionString("ElsaDatabase"));
            ef.RunMigrations = builder.Environment.IsDevelopment();
        });
    });
    
    elsa.UseWorkflowRuntime(runtime =>
    {
        runtime.UseEntityFrameworkCore(ef =>
        {
            ef.UsePostgreSql(builder.Configuration.GetConnectionString("ElsaDatabase"));
            ef.RunMigrations = builder.Environment.IsDevelopment();
        });
    });
    
    elsa.UseWorkflowsApi();
    elsa.UseHttp();
});

var app = builder.Build();

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseWorkflowsApi();
app.MapControllers();

app.Run();
```

**Connection String (appsettings.json):**

```json
{
  "ConnectionStrings": {
    "ElsaDatabase": "Host=localhost;Database=elsa_workflows;Username=elsa;Password=your_secure_password"
  }
}
```

### With SQL Server

```csharp
elsa.UseWorkflowManagement(management =>
{
    management.UseEntityFrameworkCore(ef =>
    {
        ef.UseSqlServer(builder.Configuration.GetConnectionString("ElsaDatabase"));
        ef.RunMigrations = builder.Environment.IsDevelopment();
    });
});

elsa.UseWorkflowRuntime(runtime =>
{
    runtime.UseEntityFrameworkCore(ef =>
    {
        ef.UseSqlServer(builder.Configuration.GetConnectionString("ElsaDatabase"));
        ef.RunMigrations = builder.Environment.IsDevelopment();
    });
});
```

**SQL Server Connection String:**

```json
{
  "ConnectionStrings": {
    "ElsaDatabase": "Server=localhost;Database=ElsaWorkflows;User Id=elsa_user;Password=your_secure_password;TrustServerCertificate=true"
  }
}
```

## Step 3: Initialize Database

After configuring persistence, you need to create the database schema.

### Option A: Automatic Migrations (Development)

For development environments, configure Elsa's EF Core persistence features to run migrations on startup:

```csharp
elsa.UseWorkflowManagement(management =>
{
    management.UseEntityFrameworkCore(ef =>
    {
        ef.UsePostgreSql(builder.Configuration.GetConnectionString("ElsaDatabase"));
        ef.RunMigrations = builder.Environment.IsDevelopment();
    });
});

elsa.UseWorkflowRuntime(runtime =>
{
    runtime.UseEntityFrameworkCore(ef =>
    {
        ef.UsePostgreSql(builder.Configuration.GetConnectionString("ElsaDatabase"));
        ef.RunMigrations = builder.Environment.IsDevelopment();
    });
});
```

There is no `MigrateElsaDatabaseAsync` extension method. In Elsa's source, the real configuration property is `PersistenceFeatureBase.RunMigrations`; Elsa's EF Core modules register `RunMigrationsStartupTask<TDbContext>`, which runs EF Core `Database.MigrateAsync()` for each Elsa DbContext when `RunMigrations` is enabled.

### Option B: Manual Migration Application (Production)

For production, disable automatic migrations and apply Elsa's built-in provider migrations in a controlled deployment step:

```bash
# Install EF Core tools if not already installed
dotnet tool install --global dotnet-ef

# Apply Elsa's built-in Management and Runtime context migrations
dotnet ef database update --context ManagementElsaDbContext
dotnet ef database update --context RuntimeElsaDbContext
```

{% hint style="info" %}
**Note:** Elsa uses separate DbContexts for management data and runtime data: `ManagementElsaDbContext` and `RuntimeElsaDbContext`. Apply migrations for both contexts. You only need `dotnet ef migrations add` for your own application DbContexts or custom migration strategy, not for the built-in Elsa provider migrations.
{% endhint %}

## Common Pain Points and Solutions

Based on community feedback (issue #6), here are the most common integration challenges and how to solve them:

### 1. DbContextOptions Registration Issue

**Problem:** When you have your own `AppDbContext` that requires `DbContextOptions<AppDbContext>`, you may encounter conflicts with Elsa's internal DbContext registration.

**Symptoms:**

```
System.InvalidOperationException: Unable to resolve service for type 
'Microsoft.EntityFrameworkCore.DbContextOptions`1[YourApp.Data.AppDbContext]' 
while attempting to activate 'YourApp.Data.AppDbContext'.
```

**Solution:**

Explicitly register your `AppDbContext` with its own connection string and options:

```csharp
// Register your own DbContext first
builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseNpgsql(builder.Configuration.GetConnectionString("AppDatabase"));
    // Your DbContext configuration
});

// Then register Elsa with a different connection string
builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowManagement(management =>
    {
        management.UseEntityFrameworkCore(ef =>
        {
            // Use separate connection string for Elsa
            ef.UsePostgreSql(builder.Configuration.GetConnectionString("ElsaDatabase"));
        });
    });
    
    elsa.UseWorkflowRuntime(runtime =>
    {
        runtime.UseEntityFrameworkCore(ef =>
        {
            ef.UsePostgreSql(builder.Configuration.GetConnectionString("ElsaDatabase"));
        });
    });
});
```

**Key Points:**

* Use separate databases or schemas for Elsa and your app
* Elsa's DbContexts are registered with specific lifetimes - don't try to share them
* If you must share a database, use different connection strings with schema prefixes

### 2. Version Pinning Conflicts

**Problem:** Elsa depends on specific versions of packages like `Hangfire`, `Microsoft.EntityFrameworkCore.Design`, or `Microsoft.CodeAnalysis.*`, which may conflict with your existing dependencies.

**Symptoms:**

```
NU1605: Detected package downgrade: Microsoft.EntityFrameworkCore from 8.0.0 to 7.0.5
NU1608: Detected package version outside of dependency constraint
```

**Solutions:**

#### Strategy 1: Version Alignment

Align your EF Core versions with Elsa's requirements:

```xml
<ItemGroup>
  <!-- Explicitly specify EF Core version to match Elsa -->
  <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
  <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
  <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" />
  
  <!-- Elsa packages (replace x.y.z with the latest version from NuGet) -->
  <PackageReference Include="Elsa" Version="x.y.z" />
  <PackageReference Include="Elsa.Persistence.EFCore.PostgreSql" Version="x.y.z" />
</ItemGroup>
```

#### Strategy 2: Binding Redirects (Framework Apps)

For .NET Framework apps, use binding redirects in `web.config`:

```xml
<runtime>
  <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
    <dependentAssembly>
      <assemblyIdentity name="Microsoft.EntityFrameworkCore" publicKeyToken="adb9793829ddae60" />
      <bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0" />
    </dependentAssembly>
  </assemblyBinding>
</runtime>
```

#### Strategy 3: Update Dependencies

Update your existing packages to match Elsa's requirements:

```bash
# Update all EF Core packages
dotnet add package Microsoft.EntityFrameworkCore --version 8.0.0
dotnet add package Microsoft.EntityFrameworkCore.Design --version 8.0.0
dotnet add package Microsoft.EntityFrameworkCore.Tools --version 8.0.0

# Update Hangfire if used
dotnet add package Hangfire.Core --version 1.8.0
dotnet add package Hangfire.AspNetCore --version 1.8.0
```

### 3. Swagger / Swashbuckle Schema Conflicts

**Problem:** When adding Swagger/Swashbuckle to document your API, you may encounter schema ID conflicts with Elsa's API endpoints.

**Symptoms:**

```
Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorException: 
Conflicting schemaIds: Duplicate schemaIds detected for types 
Elsa.Workflows.Core.Models.WorkflowDefinition and YourApp.Models.WorkflowDefinition
```

**Solutions:**

#### Solution 1: Custom Schema ID Generation

Configure Swashbuckle to generate unique schema IDs:

```csharp
builder.Services.AddSwaggerGen(options =>
{
    options.CustomSchemaIds(type =>
    {
        // Include namespace to avoid conflicts
        return type.FullName?.Replace("+", ".");
    });
    
    // Or use a more sophisticated approach
    options.CustomSchemaIds(type =>
    {
        if (type.FullName?.StartsWith("Elsa") == true)
        {
            return "Elsa_" + type.Name;
        }
        return type.Name;
    });
});
```

#### Solution 2: Exclude Elsa Endpoints from Swagger

If you don't need to document Elsa's API endpoints:

```csharp
builder.Services.AddSwaggerGen(options =>
{
    options.DocInclusionPredicate((docName, apiDesc) =>
    {
        // Exclude Elsa API endpoints from Swagger documentation
        var actionDescriptor = apiDesc.ActionDescriptor;
        var controllerName = actionDescriptor.RouteValues["controller"];
        
        if (controllerName?.StartsWith("Elsa") == true)
        {
            return false;
        }
        
        return true;
    });
});
```

#### Solution 3: Multiple Swagger Documents

Create separate Swagger documents for your API and Elsa:

```csharp
builder.Services.AddSwaggerGen(options =>
{
    // Your API documentation
    options.SwaggerDoc("v1", new OpenApiInfo 
    { 
        Title = "My App API", 
        Version = "v1" 
    });
    
    // Elsa API documentation
    options.SwaggerDoc("elsa", new OpenApiInfo 
    { 
        Title = "Elsa Workflows API", 
        Version = "v1" 
    });
    
    options.DocInclusionPredicate((docName, apiDesc) =>
    {
        var controllerName = apiDesc.ActionDescriptor.RouteValues["controller"];
        
        if (docName == "elsa")
        {
            return controllerName?.StartsWith("Elsa") == true;
        }
        
        return controllerName?.StartsWith("Elsa") != true;
    });
});

// In the middleware pipeline
app.UseSwagger();
app.UseSwaggerUI(options =>
{
    options.SwaggerEndpoint("/swagger/v1/swagger.json", "My App API");
    options.SwaggerEndpoint("/swagger/elsa/swagger.json", "Elsa Workflows API");
});
```

## Authentication and Authorization

For production deployments, you'll need to secure Elsa's API endpoints. This section provides a high-level overview; see [Authentication & Authorization](/guides/authentication) for identity and access configuration, then [Security & Hardening](/guides/security) for transport, ingress, secrets, and operational controls.

### Quick Overview

**Development (Disable Auth):**

* See [Disable Authentication in Development](/guides/authentication/disable-authentication)
* Not recommended for production

**Production Options:**

* **Elsa.Identity**: Built-in identity system with user management
* **API Keys**: Simple token-based authentication
* **OIDC/OAuth2**: Integration with Azure AD, Auth0, Keycloak, etc.
* See [Direct OpenID Connect](/guides/authentication/direct-openid-connect)

### Basic Identity Setup

```csharp
builder.Services.AddElsa(elsa =>
{
    elsa
        .UseIdentity(identity =>
        {
            identity.UseConfigurationBasedUserProvider(options => builder.Configuration.GetSection("Identity").Bind(options));
            identity.UseConfigurationBasedApplicationProvider(options => builder.Configuration.GetSection("Identity").Bind(options));
            identity.UseConfigurationBasedRoleProvider(options => builder.Configuration.GetSection("Identity").Bind(options));
        })
        .UseDefaultAuthentication()
        .UseWorkflowManagement()
        .UseWorkflowRuntime()
        .UseWorkflowsApi();
});

// Enable authentication middleware
app.UseAuthentication();
app.UseAuthorization();
```

## Testing Your Integration

### 1. Verify Elsa Services are Registered

Create a simple controller to check if Elsa is loaded:

```csharp
using Elsa.Workflows.Runtime;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class HealthController : ControllerBase
{
    private readonly IWorkflowRuntime _workflowRuntime;

    public HealthController(IWorkflowRuntime workflowRuntime)
    {
        _workflowRuntime = workflowRuntime;
    }

    [HttpGet("elsa")]
    public IActionResult ElsaHealth()
    {
        return Ok(new 
        { 
            elsaLoaded = _workflowRuntime != null,
            message = "Elsa Workflows is integrated successfully"
        });
    }
}
```

### 2. Check API Endpoints

With the app running, navigate to:

* `https://localhost:5001/elsa/api/workflow-definitions` - List workflow definitions
* `https://localhost:5001/swagger` - Swagger UI (if configured)

### 3. Verify Database

Check that Elsa tables were created:

**PostgreSQL:**

```sql
SELECT table_name 
FROM information_schema.tables 
WHERE table_schema = 'public' 
  AND table_name LIKE 'Elsa%';
```

**SQL Server:**

```sql
SELECT TABLE_NAME 
FROM INFORMATION_SCHEMA.TABLES 
WHERE TABLE_NAME LIKE 'Elsa%';
```

You should see tables like:

* `Elsa_WorkflowDefinitions`
* `Elsa_WorkflowInstances`
* `Elsa_Bookmarks`
* And others

## Next Steps

Now that Elsa is integrated into your app:

1. **Create your first workflow**: Use [Elsa Studio](/application-types/elsa-studio) or programmatic workflow definitions
2. **Add workflow activities**: Extend Elsa with [Custom Activities](/extensibility/custom-activities)
3. **Secure your deployment**: Configure [authentication and authorization](/guides/authentication)
4. **Deploy to production**: Follow the [Kubernetes Deployment Guide](/guides/deployment/kubernetes) or [Clustering Guide](/guides/clustering)
5. **Monitor workflows**: Set up observability and logging

## Related Documentation

* [Elsa Server Setup](/application-types/elsa-server)
* [Database Configuration](/getting-started/database-configuration)
* [Persistence Guide](/guides/persistence)
* [Security & Hardening](/guides/security)
* [Blazor Dashboard Integration](/guides/integration/blazor-dashboard)
* [Troubleshooting Guide](/guides/troubleshooting)

## Troubleshooting

### Services Not Resolving

**Problem:** `IWorkflowRuntime` or other Elsa services not found in DI.

**Solution:** Ensure you called `AddElsa()` before `builder.Build()`:

```csharp
builder.Services.AddElsa(elsa => { /* config */ });
var app = builder.Build();  // After AddElsa
```

### Database Connection Fails

**Problem:** `Npgsql.NpgsqlException: Connection refused`

**Solutions:**

* Verify database server is running
* Check connection string format
* Ensure firewall allows connections
* Test connection with `psql` or `sqlcmd`

### Migrations Not Applied

**Problem:** Tables not created after running migrations.

**Solution:** For development startup migrations, ensure `RunMigrations` is enabled for both Elsa EF Core persistence features: workflow management and workflow runtime.

For manual deployment, apply Elsa's built-in migrations for both contexts:

```bash
dotnet ef database update --context ManagementElsaDbContext
dotnet ef database update --context RuntimeElsaDbContext
```

***

**Last Updated:** 2026-06-02\
**Addresses Issues:** #6


# Authentication & Authorization

Choose and configure authentication and authorization for Elsa Server, Elsa Studio, users, and API clients.

Authentication establishes who a caller is. Authorization determines what that caller may do. In an Elsa deployment, those decisions span Elsa Server, Elsa Studio, and any workflow endpoints exposed by the `HttpEndpoint` activity.

Use this section to choose an authentication topology and configure access to the Elsa API. Use [Security & Hardening](/guides/security) for secrets, workflow ingress, bearer-style resume URLs, TLS, rate limiting, and production controls.

## Choose an authentication path

| Scenario                                                                                          | Start here                                                                             |
| ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Elsa manages users, roles, tokens, and API applications                                           | [Elsa Identity](/guides/authentication/elsa-identity)                                  |
| A service or automation client calls the Elsa API                                                 | [API Keys](/guides/authentication/api-keys)                                            |
| Studio signs in directly with an upstream OpenID Connect provider                                 | [Direct OpenID Connect](/guides/authentication/direct-openid-connect)                  |
| Elsa brokers one or more upstream providers and manages connections, identity links, and sessions | [External Authentication](/guides/authentication/external-authentication)              |
| The host supplies another ASP.NET Core authentication scheme                                      | [Custom Authentication](/guides/authentication/custom-authentication)                  |
| Authentication must be disabled for an isolated local environment                                 | [Disable Authentication in Development](/guides/authentication/disable-authentication) |

{% hint style="warning" %}
External Authentication is available as of Elsa 3.8, which is currently under development. Preview packages are published through the Elsa Feedz.io feed; they are not stable releases.
{% endhint %}

Direct OpenID Connect and External Authentication are different topologies. External Authentication is the strategic successor for new deployments that need Elsa-managed provider connections and sessions. Direct OIDC remains supported throughout Elsa 3.x and is not formally deprecated in Elsa 3.8.

## Understand the security boundaries

### Elsa Server API

Elsa API endpoints authenticate through ASP.NET Core and authorize through Elsa `permissions` claims. Elsa Identity issues those claims from assigned roles. External schemes must provide them directly or map trusted upstream roles, groups, or scopes into them.

See [Elsa API Permissions](/guides/authentication/permissions) for the permission model, endpoint families, and starter role templates.

### Elsa Studio

Studio is an API client, not a second authorization authority. Its selected authentication provider signs the user in and obtains credentials for Elsa Server. The server still decides whether each API operation is allowed.

A successful Studio login therefore does not guarantee access to workflow definitions, instances, designer metadata, or administration screens. Missing permissions normally surface as `403 Forbidden` responses from the API.

### Workflow HTTP endpoints

Routes exposed by the `HttpEndpoint` activity have their own `Authorize` and `Policy` settings. They are separate from Elsa API permissions. A public workflow route does not make `/elsa/api/*` public, and API permissions do not secure a workflow route automatically.

See [HTTP Endpoint Security](/guides/security/http-endpoint-security).

## Recommended reading paths

For an Elsa-managed deployment:

1. Configure [Elsa Identity](/guides/authentication/elsa-identity).
2. Define least-privilege roles with [Elsa API Permissions](/guides/authentication/permissions).
3. Add [API keys](/guides/authentication/api-keys) for service clients when needed.
4. Complete the [production hardening](/guides/security/production-hardening) checklist.

For an external identity provider:

1. Choose [Direct OpenID Connect](/guides/authentication/direct-openid-connect) or [External Authentication](/guides/authentication/external-authentication).
2. Configure the Elsa API permission claims required by Studio and operators.
3. Review provider secrets, redirects, TLS, and deployment controls under [Security & Hardening](/guides/security).

## Related sections

* [Security & Hardening](/guides/security)
* [Elsa API & Client](/guides/api-client)
* [Elsa Studio integration](/guides/studio/integration)
* [Hosting Elsa in an existing app](/guides/onboarding/hosting-elsa-in-existing-app)


# Elsa Identity

Configure Elsa's built-in identity system for users, roles, tokens, and API applications.

Elsa Identity is the built-in option when Elsa should manage users, roles, access and refresh tokens, and API applications. Roles contain Elsa permission strings; issued JWTs and authenticated API keys expose those permissions as `permissions` claims.

## Register the identity modules

Install the `Elsa.Identity` package and configure identity before the workflow API:

```csharp
using Elsa.Extensions;

var builder = WebApplication.CreateBuilder(args);
var identitySection = builder.Configuration.GetSection("Identity");
var tokenSection = identitySection.GetSection("Tokens");

builder.Services.AddElsa(elsa =>
{
    elsa
        .UseIdentity(identity =>
        {
            identity.TokenOptions += options => tokenSection.Bind(options);
            identity.UseConfigurationBasedUserProvider(options =>
                identitySection.Bind(options));
            identity.UseConfigurationBasedApplicationProvider(options =>
                identitySection.Bind(options));
            identity.UseConfigurationBasedRoleProvider(options =>
                identitySection.Bind(options));
        })
        .UseDefaultAuthentication()
        .UseWorkflowManagement()
        .UseWorkflowRuntime()
        .UseWorkflowsApi();
});

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.UseWorkflowsApi();
app.Run();
```

`UseIdentity` registers the identity services and selected providers. `UseDefaultAuthentication` enables Elsa JWT bearer and API-key authentication. The authentication and authorization middleware must run before the Elsa API endpoints.

## Configure tokens, users, and roles

The configuration-based providers bind the `Identity` section. A minimal shape is:

```json
{
  "Identity": {
    "Tokens": {
      "SigningKey": "set-outside-source-control",
      "Issuer": "https://elsa.example",
      "Audience": "https://elsa.example",
      "AccessTokenLifetime": "01:00:00",
      "RefreshTokenLifetime": "7.00:00:00"
    },
    "Roles": [
      {
        "Id": "workflow-viewer",
        "Name": "Workflow Viewer",
        "Permissions": [
          "read:workflow-definitions",
          "read:workflow-instances",
          "read:activity-execution"
        ]
      }
    ],
    "Users": [
      {
        "Id": "operator-1",
        "Name": "operator",
        "HashedPassword": "set-outside-source-control",
        "HashedPasswordSalt": "set-outside-source-control",
        "Roles": ["workflow-viewer"]
      }
    ]
  }
}
```

See the focused [example configuration](https://github.com/elsa-workflows/elsa-gitbook/tree/main/guides/authentication/examples/appsettings-identity.json). Do not commit signing keys, passwords, API keys, client secrets, or their production source material.

Configuration-based providers are convenient for small or deployment-owned sets of identities. Elsa also exposes store-based providers for applications that manage users, applications, and roles through durable stores. Choose one provider for each identity type and configure persistence appropriate to the deployment.

## Configure Studio

Select Elsa Identity in the Studio host:

```json
{
  "Backend": {
    "Url": "https://elsa.example/elsa/api"
  },
  "Authentication": {
    "Provider": "ElsaIdentity"
  }
}
```

Both the Blazor Server and WebAssembly Studio hosts support Elsa Identity. Studio signs in against the Elsa backend and sends the resulting bearer token with API requests.

## Production guidance

* Store signing keys and credential material outside source control.
* Use HTTPS for Studio, Elsa Server, and every token exchange.
* Prefer short-lived access tokens and protect refresh tokens.
* Give roles named permissions; reserve `*` for tightly controlled administrators.
* Use durable providers and shared cryptographic configuration in scaled-out deployments.
* Rotate signing keys and credentials through a planned process that accounts for already issued tokens.

## Related guides

* [API Keys](/guides/authentication/api-keys)
* [Elsa API Permissions](/guides/authentication/permissions)
* [Security & Hardening](/guides/security)
* [Secrets Management](/guides/security/secrets-management)


# API Keys

Authenticate machine-to-machine Elsa API clients with application API keys.

Elsa API keys are intended for applications, automation, command-line tools, and other machine-to-machine clients. They are not a replacement for user sign-in or delegated OpenID Connect access.

API-key authentication is enabled by `UseDefaultAuthentication()` together with Elsa Identity. Each key belongs to an Elsa Identity application. The application's roles determine the `permissions` claims granted to the caller.

## Create an application

The identity application endpoint creates a client ID, client secret, and API key, stores their hashes, and returns the generated credentials. The caller must have `create:application` and satisfy the identity security-root policy.

With the default Elsa API prefix, submit:

```http
POST /elsa/api/identity/applications
Authorization: Bearer <administrator-token>
Content-Type: application/json

{
  "name": "Order automation",
  "roles": ["workflow-runner"]
}
```

Capture the returned API key securely. Treat it as a secret and do not log it, place it in source control, or expose it to browser code.

Configuration-owned applications can instead supply `HashedApiKey` and `HashedApiKeySalt` in the `Identity:Applications` section. Store the original key in a secret manager and deploy only the hash and salt to Elsa Server.

## Call the Elsa API

Send the key through the `Authorization` header:

```http
GET /elsa/api/workflow-definitions
Authorization: ApiKey <api-key>
```

The Elsa .NET API client can configure the same scheme with `AddDefaultApiClientsUsingApiKey(...)`.

## Use least-privilege roles

Create a role for the exact operations performed by the client. For example, a service that only starts published workflows generally needs definition read and execute permissions, not workflow editing, identity administration, or runtime administration.

See [Elsa API Permissions](/guides/authentication/permissions) for current permission names and starter role templates.

## Rotate and revoke keys

Plan rotation before issuing a production key:

1. Create a replacement application or credential.
2. Deploy the new key to the client through its secret store.
3. Verify requests use the new credential.
4. Remove or disable the old application credential.
5. Investigate any use of the revoked key.

Do not send API keys in query strings. Avoid embedding them in WebAssembly, JavaScript bundles, mobile packages, or other distributable clients where the secret cannot be protected.

## Related guides

* [Elsa Identity](/guides/authentication/elsa-identity)
* [Elsa API Permissions](/guides/authentication/permissions)
* [Secrets Management](/guides/security/secrets-management)
* [Production Hardening](/guides/security/production-hardening)


# Direct OpenID Connect

Release-backed guide to wiring Elsa Server and Elsa Studio to external OpenID Connect identity providers in Elsa 3.8.

This guide covers the identity-provider integration points that are actually present in `release/3.8.0` across `elsa-core` and `elsa-studio`.

{% hint style="info" %}
**Choosing an authentication path**

[External Authentication](/guides/authentication/external-authentication) is the strategic successor for new deployments: Elsa Server brokers one or more providers and issues the credentials consumed by Studio. Direct Studio OIDC remains supported throughout Elsa 3.x and is not formally deprecated in Elsa 3.8.
{% endhint %}

## Overview

Use direct OIDC when:

* Elsa Server should trust tokens issued by an external provider instead of Elsa's built-in identity system.
* Elsa Studio should sign users in with the same OpenID Connect provider and forward bearer tokens to Elsa Server.

This page is intentionally narrower than a generic identity-platform guide. Elsa 3.8 ships first-class Studio support for OpenID Connect, and Elsa Server authorizes API calls based on ASP.NET Core authentication plus Elsa-specific `permissions` claims.

If Elsa should own the upstream authorization-code flow, connection lifecycle, identity links, or Studio SSO administration, use [External Authentication](/guides/authentication/external-authentication) instead. The broker is an opt-in path distinct from direct bearer-token validation.

## What Elsa 3.8 Actually Expects

### Elsa Server

In the standalone `Elsa.Server.Web` host, the built-in path is:

```csharp
elsa
    .UseIdentity(...)
    .UseDefaultAuthentication();
```

That helper configures JWT bearer validation from Elsa `IdentityTokenOptions` and also adds API-key support. It is the correct choice when Elsa itself issues the JWTs or API keys.

For external identity providers, Elsa does not ship a provider-specific server module. Your host application is responsible for:

* configuring ASP.NET Core authentication and authorization
* validating the external bearer tokens
* mapping external roles, groups, or scopes into Elsa `permissions` claims

Elsa API endpoints then authorize against those `permissions` claims. In `release/3.8.0`, the claim type is literally `permissions`, and `*` grants all Elsa permissions.

### Elsa Studio

Elsa Studio ships first-class OpenID Connect support for both default hosts:

* `Elsa.Studio.Host.Server`
* `Elsa.Studio.Host.Wasm`

Both hosts read:

```json
{
  "Authentication": {
    "Provider": "OpenIdConnect",
    "OpenIdConnect": {
      "Authority": "https://your-idp",
      "ClientId": "your-client-id",
      "AuthenticationScopes": ["openid", "profile", "offline_access"],
      "BackendApiScopes": ["api://your-api/elsa-server-api"]
    }
  }
}
```

In `release/3.8.0`:

* Blazor Server defaults to `Authentication:Provider = ElsaIdentity`
* Blazor WebAssembly defaults to `Authentication:Provider = OpenIdConnect`
* Blazor Server uses `/signin-oidc` and `/signout-callback-oidc` unless overridden
* Blazor WebAssembly uses `/authentication/login-callback` and `/authentication/logout-callback`
* Studio logout starts at `/authentication/logout`

## Recommended Topology

Use a single OpenID Connect provider for both Studio and Server when you want SSO and centralized authorization:

1. Register an API or resource for Elsa Server in your identity provider.
2. Configure Elsa Server to validate bearer tokens for that audience.
3. Map the provider's roles, groups, or scopes to Elsa `permissions` claims.
4. Configure Elsa Studio `Authentication:OpenIdConnect` to request sign-in scopes plus the backend API scope.

If you only need machine-to-machine access, you can stop at step 2 and issue tokens directly to service clients without using Studio.

## Server Setup Pattern

This host-side pattern matches Elsa's 3.8.0 authorization contract for external bearer tokens:

```csharp
using System.Security.Claims;
using Elsa;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = builder.Configuration["Oidc:Authority"];
        options.Audience = builder.Configuration["Oidc:Audience"];
        options.MapInboundClaims = false;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            NameClaimType = "name",
            RoleClaimType = "role",
            ValidateIssuer = true,
            ValidateAudience = true
        };

        options.Events = new JwtBearerEvents
        {
            OnTokenValidated = context =>
            {
                var identity = (ClaimsIdentity)context.Principal!.Identity!;

                // Map provider-specific claims into Elsa permissions.
                foreach (var scope in context.Principal.FindAll("scope").Select(x => x.Value))
                {
                    if (scope == "elsa.admin")
                        identity.AddClaim(new Claim(PermissionNames.ClaimType, PermissionNames.All));
                }

                foreach (var role in context.Principal.FindAll("role").Select(x => x.Value))
                {
                    if (role == "elsa-operator")
                        identity.AddClaim(new Claim(PermissionNames.ClaimType, "read:workflow-definitions"));
                }

                return Task.CompletedTask;
            }
        };
    });

builder.Services.AddAuthorization();

builder.Services.AddElsa(elsa =>
{
    elsa
        .UseWorkflowManagement()
        .UseWorkflowRuntime()
        .UseWorkflowsApi();
});

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapWorkflowsApi();
app.Run();
```

### Why the `permissions` Claim Matters

Elsa endpoint permissions are not expressed as ASP.NET Core policies. They are checked as permission claims on the authenticated principal.

That means your external identity provider integration is only complete when one of these is true:

* the provider issues `permissions` claims with Elsa permission values
* your ASP.NET Core host maps other claims into `permissions`

## Studio Setup Pattern

### Blazor Server Studio

Use a confidential client when the provider requires a client secret:

```json
{
  "Backend": {
    "Url": "https://elsa.example.com/elsa/api"
  },
  "Authentication": {
    "Provider": "OpenIdConnect",
    "OpenIdConnect": {
      "Authority": "https://login.example.com/realms/acme",
      "ClientId": "elsa-studio-server",
      "ClientSecret": "set-via-secret-store",
      "AuthenticationScopes": ["openid", "profile", "offline_access"],
      "BackendApiScopes": ["elsa-api"],
      "SaveTokens": true
    }
  }
}
```

Register these redirect URIs unless you override the defaults:

* `https://studio.example.com/signin-oidc`
* `https://studio.example.com/signout-callback-oidc`

### Blazor WebAssembly Studio

Use a public SPA client and do not configure a client secret:

```json
{
  "Backend": {
    "Url": "https://elsa.example.com/elsa/api"
  },
  "Authentication": {
    "Provider": "OpenIdConnect",
    "OpenIdConnect": {
      "Authority": "https://login.example.com/realms/acme",
      "ClientId": "elsa-studio-wasm",
      "AuthenticationScopes": ["openid", "profile", "offline_access"],
      "BackendApiScopes": ["elsa-api"]
    }
  }
}
```

Register these redirect URIs unless you override the defaults:

* `https://studio.example.com/authentication/login-callback`
* `https://studio.example.com/authentication/logout-callback`

### Authentication Scopes vs Backend API Scopes

Keep the two scope lists separate:

* `AuthenticationScopes`: scopes needed for signing the user in
* `BackendApiScopes`: scopes needed on tokens sent to Elsa Server

This separation matters for providers such as Microsoft Entra ID, where a token request must target one resource audience at a time.

## Provider Notes

### Microsoft Entra ID

* Prefer a tenant-specific authority such as `https://login.microsoftonline.com/{tenant-id}/v2.0`
* Register Studio WASM as a SPA/public client
* Register Studio Server as a confidential web app if you need a client secret
* Put the Elsa API scope in `BackendApiScopes`
* Leave `GetClaimsFromUserInfoEndpoint` disabled unless your app registration explicitly supports `userinfo`

### Auth0

* Set `Authority` to your tenant URL such as `https://acme.us.auth0.com/`
* Define an API for Elsa Server and request that audience or scope from Studio
* If Auth0 already emits a `permissions` array, map those values directly to Elsa permissions where possible

### Keycloak, Okta, OpenIddict, IdentityServer, Generic OIDC

* Use discovery-based OpenID Connect metadata through `Authority`
* Use authorization code flow for Studio
* Use PKCE for public/browser clients
* Make sure the API access token audience matches Elsa Server
* Add explicit mappers if your provider emits roles or groups but not Elsa `permissions` claims

## Troubleshooting

### Studio signs in, but Elsa API calls return 401

Check these first:

* `Backend:Url` points to the actual Elsa API base URL
* the token audience matches the Elsa API registration
* the token presented to Elsa Server contains `permissions` claims, or your host maps other claims into `permissions`
* `app.UseAuthentication()` runs before `app.UseAuthorization()`

### Login callback returns 404

Your identity-provider redirect URI does not match the Studio host model:

* Blazor Server: `/signin-oidc`
* Blazor WebAssembly: `/authentication/login-callback`

### User is authenticated, but actions are still forbidden

The common cause is missing Elsa permission claims. Inspect the final authenticated principal on the server and verify claim type `permissions` contains either:

* the specific permission required by the endpoint
* `*` for full access

### OIDC `userinfo` calls fail with 401

The shipped Studio hosts already default `GetClaimsFromUserInfoEndpoint` to `false`. Keep it that way unless your provider specifically requires and allows that extra call.

## Related Guides

* [Security & Hardening](/guides/security)
* [Authentication & Authorization](/guides/authentication)
* [Studio Designer Integration](/guides/studio/integration)
* [Blazor Dashboard Integration](/guides/integration/blazor-dashboard)
* [External Authentication](/guides/authentication/external-authentication)


# External Authentication

Install, configure, operate, and migrate to Elsa's brokered External Authentication capability for Elsa Server and Elsa Studio.

{% hint style="warning" %}
**Preview in Elsa 3.8**

External Authentication is available starting with **Elsa 3.8**, which is currently under active development. The packages are preview-only and are published through the [Elsa Feedz preview feed](/getting-started/packages#previews). APIs, configuration, persistence migrations, and Studio screens can still change before general availability.
{% endhint %}

External Authentication is Elsa's brokered sign-in capability. Elsa Server becomes the relying party for one or more upstream identity providers, resolves the external identity to an Elsa User, and issues the Elsa credentials used by Studio. Provider tokens and client secrets do not pass through Studio.

Use this guide to:

* install the Core, provider-adapter, persistence, secret-binding, and Studio modules;
* register conventional Elsa hosts or CShells/Modular Server features;
* connect an OpenID Connect provider;
* configure both Blazor Server and WebAssembly Studio hosts;
* administer connections, identity links, previews, and sessions in Studio;
* harden single-node and multi-node deployments; and
* migrate safely from direct Studio OpenID Connect.

## Strategic Direction and Direct OIDC

Brokered External Authentication is the strategic successor to Studio's direct OpenID Connect mode and is the recommended direction for new deployments. It is especially useful when providers must be managed centrally or at runtime, when more than one login method is required, or when administrators need identity-link and session controls.

Direct OIDC is **not deprecated in Elsa 3.8**. It remains a supported, selectable mode throughout Elsa 3.x. Any removal requires parity, migration tooling, advance notice, and a future major release. See [Migrate from Direct OIDC](/guides/authentication/external-authentication/migration-from-direct-oidc) for a staged, reversible transition.

## Architecture

```mermaid
sequenceDiagram
    actor User
    participant Studio as Elsa Studio
    participant Elsa as Elsa Server broker
    participant IdP as OIDC provider

    Studio->>Elsa: Discover login methods
    User->>Studio: Select a login method
    Studio->>Elsa: Begin sign-in with PKCE
    Elsa->>IdP: Redirect to provider
    IdP->>Elsa: Provider authorization code
    Elsa->>Elsa: Resolve/link Elsa User and permissions
    Elsa->>Studio: Short-lived, single-use Elsa code
    Studio->>Elsa: Exchange code with PKCE
    Elsa-->>Studio: Elsa access and refresh credentials
```

The upstream provider client and the Elsa **Authentication Client** are separate registrations:

* The provider client describes Elsa Server to the OIDC provider and owns the provider client ID, client secret, scopes, and Elsa callback URI.
* The Elsa Authentication Client describes Studio to the broker and owns Studio's exact callback, logout callback, allowed return paths, origin, and confidential-client secret where applicable. It grants no Elsa permissions.

## Development quickstart

Use this shortest safe path for an isolated development environment:

1. Add the Feedz preview source and install the Core foundation plus the OIDC adapter as shown in [Installation](/guides/authentication/external-authentication/installation#configure-the-preview-feed).
2. Enable Elsa Identity, External Authentication, and the OIDC adapter. Use the in-memory stores only for this first single-node run.
3. Configure one OIDC connection, one Studio Authentication Client, the public Elsa callback base URI, and a local Elsa login using the [minimal configuration](/guides/authentication/external-authentication/configuration#minimal-openid-connect-configuration).
4. Register the normal and preview callback URIs displayed by Elsa with the provider. The [Keycloak walkthrough](/guides/authentication/external-authentication/keycloak) shows the complete provider side.
5. Configure either Studio Server or Studio WebAssembly using [Studio integration](/guides/authentication/external-authentication/studio-integration#packages-and-host-selection). Neither host model is preferred; use the client type that matches the host.
6. Start Elsa Server and Studio, sign in through the retained local recovery account, then open **Administration → Identity & access → Identity provider connections**.
7. Validate and test the connection, run Preview sign-in, enable the connection, sign out, and complete a normal provider sign-in with a non-admin test user.

Before retaining data, restarting the host, or adding another node, replace the development stores with [durable persistence and shared keys](/guides/authentication/external-authentication/production).

## Choose Your Path

| You are...                                   | Start here                                                                                                                                                                           |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Evaluating the feature                       | [Install External Authentication](/guides/authentication/external-authentication/installation), then [Keycloak Walkthrough](/guides/authentication/external-authentication/keycloak) |
| Integrating a conventional Elsa host         | [Conventional host installation](/guides/authentication/external-authentication/installation#classic-addelsa-host)                                                                   |
| Configuring CShells/Modular Server           | [Modular Server installation](/guides/authentication/external-authentication/installation#cshells-modular-server)                                                                    |
| Integrating Studio Server or WebAssembly     | [Studio Integration](/guides/authentication/external-authentication/studio-integration)                                                                                              |
| Configuring connections and security policy  | [Configuration Reference](/guides/authentication/external-authentication/configuration)                                                                                              |
| Administering a running environment          | [Administration in Studio](/guides/authentication/external-authentication/administration)                                                                                            |
| Preparing a durable or multi-node deployment | [Production and Security](/guides/authentication/external-authentication/production)                                                                                                 |
| Automating management                        | [REST API](/guides/authentication/external-authentication/api)                                                                                                                       |
| Migrating from direct OIDC                   | [Migrate from Direct OIDC](/guides/authentication/external-authentication/migration-from-direct-oidc)                                                                                |
| Diagnosing a failed sign-in                  | [Troubleshooting](/guides/authentication/external-authentication/troubleshooting)                                                                                                    |

## Capability Modules

The feature is intentionally split across Elsa Core and Elsa Studio:

| Area   | Package                                            | Purpose                                                             |
| ------ | -------------------------------------------------- | ------------------------------------------------------------------- |
| Core   | `Elsa.ExternalAuthentication`                      | Broker, configuration, APIs, policies, in-memory development stores |
| Core   | `Elsa.ExternalAuthentication.OpenIdConnect`        | Built-in OpenID Connect protocol adapter                            |
| Core   | `Elsa.ExternalAuthentication.Secrets`              | Optional bridge for Elsa-managed secret values                      |
| Core   | `Elsa.ExternalAuthentication.Persistence.EFCore.*` | Durable provider-specific persistence                               |
| Studio | `Elsa.Studio.ExternalAuthentication`               | Login-method and administration UI                                  |
| Studio | `Elsa.Studio.ExternalAuthentication.BlazorServer`  | Confidential server-side broker client                              |
| Studio | `Elsa.Studio.ExternalAuthentication.BlazorWasm`    | Public browser broker client with PKCE                              |

Both Studio hosting models are supported. Neither is universally preferred: [compare their trust and storage boundaries](/guides/authentication/external-authentication/studio-integration#packages-and-host-selection) and choose the model that fits your topology.

## Core Concepts

* **Identity Provider Connection**: Elsa's versioned trust relationship with an upstream provider. It can be deployment-owned configuration or administrator-owned persisted data.
* **Authentication Client**: a deployment-owned Studio or application registration at the Elsa broker. It is not an Elsa API application and grants no permissions.
* **External Identity Link**: a tenant-scoped association between a validated provider identity and an Elsa User.
* **External Authentication Session**: bounded session metadata used to validate refresh, connection state, expiry, and revocation.
* **Unlinked Identity Policy**: determines whether a first external sign-in is rejected, creates a user, or invokes an installed matcher.
* **Permission Grant Source**: converts trusted provider information into explicit Elsa authorization grants. Upstream roles never become Elsa permissions automatically.

In the current 3.8 preview UI, these resources appear under **Administration → Identity & access** as **Identity provider connections**, **External identity links**, and **Authentication sessions**.

## Scope of This Guide

This section documents deployment and operation of the built-in OIDC capability. For custom protocol adapters, policies, user matchers, permission grant sources, descriptor schemas, and custom Studio editors, see [External Authentication extensibility](/guides/authentication/external-authentication/extensibility).


# Installation

Install the Elsa 3.8 preview External Authentication broker, its OpenID Connect adapter, optional secret bridge, and durable persistence providers.

> **Preview feature:** External Authentication is available starting with **Elsa 3.8 preview** packages. At the time of writing it is published from the [Elsa Feedz preview feed](https://f.feedz.io/elsa-workflows/elsa-3/nuget/index.json), not as a stable NuGet.org release, and remains under active development. Test upgrades in a non-production environment and expect configuration or API changes before general availability.

External Authentication makes Elsa Server a broker between Elsa Studio and one or more upstream identity providers. Elsa owns the sign-in transaction, issues the Elsa access and refresh tokens that Studio consumes, and can centrally manage connections, identity links, sessions, and permission mapping. The first supplied provider adapter is OpenID Connect.

This is different from Studio's existing direct OpenID Connect mode, in which Studio talks directly to the identity provider. See [Migration from Direct OpenID Connect](/guides/authentication/external-authentication/migration-from-direct-oidc) before changing an existing Studio host.

## Prerequisites

* An Elsa **3.8 preview** application with Elsa Identity enabled. The broker issues Elsa credentials, so it needs Elsa Identity token signing and a user/role provider.
* An upstream OpenID Connect provider with a confidential client registration for Elsa Server.
* A public HTTPS address for Elsa Server. It is used to derive the fixed provider callbacks.
* For production or multiple nodes, a relational database supported by the selected persistence provider and a shared ASP.NET Core Data Protection key store. See [Production](/guides/authentication/external-authentication/production).

Use a real secret manager, environment variables, or another configuration provider for keys and client secrets. The examples deliberately use placeholders only.

## Configure the preview feed

Add the Feedz source to `NuGet.config` alongside NuGet.org:

```xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
    <add key="Elsa 3 preview" value="https://f.feedz.io/elsa-workflows/elsa-3/nuget/index.json" />
  </packageSources>
</configuration>
```

Install preview packages and keep all Elsa Core packages on one compatible `3.8.0-preview.*` build. Pin the resolved build centrally for repeatable deployments; do not mix preview packages with a released 3.x package set.

## Choose packages

Install the foundation and at least one protocol adapter:

```bash
dotnet add package Elsa.ExternalAuthentication --prerelease
dotnet add package Elsa.ExternalAuthentication.OpenIdConnect --prerelease
```

The available packages are:

| Package                                                     | When to install it                                                                                                                                    |
| ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Elsa.ExternalAuthentication`                               | Required foundation: broker, connection model, management and broker APIs, in-memory development stores, permission mapping, and local-login support. |
| `Elsa.ExternalAuthentication.OpenIdConnect`                 | Installs the `openid-connect` upstream provider adapter.                                                                                              |
| `Elsa.ExternalAuthentication.Secrets`                       | Optional bridge to Elsa Secrets for Elsa-managed client secrets. It is not needed for secrets read from standard .NET configuration.                  |
| `Elsa.ExternalAuthentication.Persistence.EFCore`            | Shared EF Core persistence base. It is normally brought in transitively by a provider package.                                                        |
| `Elsa.ExternalAuthentication.Persistence.EFCore.Sqlite`     | EF Core persistence for SQLite.                                                                                                                       |
| `Elsa.ExternalAuthentication.Persistence.EFCore.SqlServer`  | EF Core persistence for SQL Server.                                                                                                                   |
| `Elsa.ExternalAuthentication.Persistence.EFCore.PostgreSql` | EF Core persistence for PostgreSQL.                                                                                                                   |
| `Elsa.ExternalAuthentication.Persistence.EFCore.MySql`      | EF Core persistence for MySQL.                                                                                                                        |
| `Elsa.ExternalAuthentication.Persistence.EFCore.Oracle`     | EF Core persistence for Oracle.                                                                                                                       |

For example, a SQL Server deployment adds:

```bash
dotnet add package Elsa.ExternalAuthentication.Persistence.EFCore.SqlServer --prerelease
```

The provider-specific package is required for its migrations and, in a CShells host, to make the corresponding shell feature discoverable. Identity persistence and External Authentication persistence are separate features: adding `Elsa.Persistence.EFCore.*` or enabling an Identity persistence feature does **not** make broker state durable.

## Classic `AddElsa` host

Register Elsa Identity, the broker, and the OIDC adapter. Use `BindExternalAuthenticationOptions` rather than plain configuration binding: it also reconstructs the JSON settings carried by adapters, policies, and permission grant sources.

```csharp
using Elsa.Extensions;
using Microsoft.Extensions.Configuration;

builder.Services.AddElsa(elsa =>
{
    elsa.UseIdentity(identity =>
    {
        identity.TokenOptions = options =>
        {
            options.SigningKey = builder.Configuration["Identity:SigningKey"]
                ?? throw new InvalidOperationException("Identity signing key is required.");
            options.Issuer = "https://elsa.example.com";
            options.Audience = "https://elsa.example.com";
        };
    });

    elsa.UseExternalAuthentication(feature =>
    {
        feature.ConfigureOptions = options =>
            builder.Configuration.GetSection("ExternalAuthentication")
                .BindExternalAuthenticationOptions(options);
    });
});

builder.Services.AddOpenIdConnectExternalAuthentication();
```

This registration uses in-memory stores and is appropriate only for local, single-node development. Add the selected EF Core feature for a durable host. The SQLite form illustrates the classic feature composition:

```csharp
using Elsa.ExternalAuthentication.Persistence.EFCore.Sqlite.Extensions;

builder.Services.AddElsa(elsa =>
{
    elsa.UseExternalAuthentication(externalAuthentication =>
    {
        externalAuthentication.UseEntityFrameworkCore(ef =>
            ef.UseSqlite("Data Source=external-authentication.db;Cache=Shared"));
    });
});
```

Keep this in the same Elsa configuration that enables the broker. Replace `UseSqlite` with `UseSqlServer`, `UsePostgreSql`, `UseMySql`, or `UseOracle` when using the matching provider package. Apply that provider's `ExternalAuthenticationElsaDbContext` migration through your normal migration deployment process.

If a connection or authentication client uses Elsa-managed secrets, register the optional bridge as well:

```csharp
builder.Services.AddElsaSecretsExternalAuthentication();
```

## CShells / Modular Server

CShells hosts discover External Authentication through shell features. Ensure the application references the adapter package and, for durable state, the matching persistence package. Enable the feature names in the shell configuration:

```json
{
  "CShells": {
    "Shells": {
      "Default": {
        "Features": {
          "ExternalAuthentication": {
            "Redirects": {
              "ExternalCallbackBaseUri": "https://elsa.example.com/elsa/api/"
            }
          },
          "OpenIdConnectExternalAuthentication": {},
          "SqlServerExternalAuthenticationPersistence": {
            "ConnectionString": "<external-authentication-database-connection-string>"
          }
        }
      }
    }
  }
}
```

Replace `Default` with the configured shell name when the host uses another shell. The feature-local settings path is therefore `CShells:Shells:<shell-name>:Features:ExternalAuthentication`.

The persistence feature names are `SqliteExternalAuthenticationPersistence`, `SqlServerExternalAuthenticationPersistence`, `PostgreSqlExternalAuthenticationPersistence`, `MySqlExternalAuthenticationPersistence`, and `OracleExternalAuthenticationPersistence`.

To resolve managed bindings from Elsa Secrets, also enable `ElsaSecretsExternalAuthentication`; it depends on both `ExternalAuthentication` and the Elsa `Secrets` feature. The OIDC feature depends on `ExternalAuthentication`. A Modular Server reference host demonstrates these packages and features.

## Confirm the installation

After startup, the broker APIs live below Elsa's API route prefix, for example `https://elsa.example.com/elsa/api/external-authentication`.

1. Configure an Authentication Client and at least one connection as described in [Configuration](/guides/authentication/external-authentication/configuration).
2. Request `GET /external-authentication/login-methods?clientId=<your-client-id>`.
3. Confirm that the response lists the local method (unless disabled) and your enabled connection, without provider settings or secrets.
4. For a persisted setup, verify that the selected External Authentication persistence feature is enabled and its migrations are applied.

The management UI is provided by the companion Elsa Studio External Authentication modules. Once those modules are installed, open **Administration → Identity & access → Identity provider connections** (`/security/external-authentication/connections`) to manage database-owned connections; configuration-owned connections are inspect-only.


# Configuration

Configure Elsa External Authentication connections, clients, callback URLs, secret bindings, identity policy, and permission mapping.

> **Elsa 3.8 preview:** This guide documents the preview External Authentication modules distributed from Feedz. The feature is under development; validate every preview upgrade before promoting it.

External Authentication has two configuration boundaries:

* **Deployment-owned configuration** defines Authentication Clients, configuration-owned connections, adapter and extension allowlists, callback base URL, provider egress policy, permission boundaries, and recovery policy.
* **Database-owned connections** are optional records managed through the APIs and Studio. They let authorized administrators create, test, preview, enable, disable, archive, and restore identity-provider connections without changing deployment configuration.

Configuration is read from `ExternalAuthentication`. In a CShells host, the same settings are nested below `CShells:Shells:<shell-name>:Features:ExternalAuthentication`.

## Minimal OpenID Connect configuration

The following sample configures a confidential Elsa Studio Server client and a configuration-owned OIDC connection. Values that are secrets remain references, never literal values.

```json
{
  "ExternalAuthentication": {
    "Redirects": {
      "ExternalCallbackBaseUri": "https://elsa.example.com/elsa/api/"
    },
    "AuthenticationClients": [
      {
        "clientId": "elsa-studio-server",
        "displayName": "Elsa Studio Server",
        "clientType": "confidential",
        "callbackUris": [
          "https://studio.example.com/authentication/external/callback"
        ],
        "logoutCallbackUris": [
          "https://studio.example.com/authentication/external/logout-callback"
        ],
        "allowedReturnPathPrefixes": ["/"],
        "secretBinding": {
          "ownership": "external",
          "resolverType": "configuration",
          "reference": "Secrets:ExternalAuthentication:StudioServerClientSecret"
        },
        "isEnabled": true
      }
    ],
    "Connections": [
      {
        "id": "contoso-workforce-configuration",
        "key": "contoso-workforce",
        "adapterType": "openid-connect",
        "adapterSettingsVersion": 2,
        "adapterSettings": {
          "mode": "discovery",
          "discoveryUrl": "https://login.example.com/.well-known/openid-configuration",
          "clientId": "elsa-server-at-contoso",
          "clientAuthenticationMethod": "client_secret_basic",
          "scopes": ["profile", "email", "groups"]
        },
        "secretBindings": {
          "clientSecret": {
            "ownership": "external",
            "resolverType": "configuration",
            "reference": "Secrets:ExternalAuthentication:ContosoProviderClientSecret",
            "expectedType": "text",
            "expectedScope": "external-authentication"
          }
        },
        "displayName": "Contoso Workforce",
        "iconId": "building",
        "displayOrder": 10,
        "isPreferred": true,
        "isEnabled": true,
        "unlinkedPolicy": {
          "type": "reject",
          "settingsVersion": 1,
          "settings": {}
        },
        "claimProjection": {
          "allowedClaimTypes": ["name", "email", "groups"],
          "redactedClaimTypes": ["email"],
          "maximumClaimCount": 50,
          "maximumValueLength": 2048,
          "maximumTotalBytes": 32768
        },
        "upstreamLogoutMode": "disabled"
      }
    ]
  }
}
```

For standard .NET configuration, set the actual values outside source control, for example:

```
Secrets__ExternalAuthentication__StudioServerClientSecret=<strong-random-client-secret>
Secrets__ExternalAuthentication__ContosoProviderClientSecret=<provider-client-secret>
```

The configuration resolver reads the key named by `reference`. It exposes only configured/resolvable status to management clients, not secret values. It can obtain values from environment variables, mounted configuration, Key Vault-style providers, or any other standard `IConfiguration` provider.

## Authentication Clients

An Authentication Client identifies Studio (or another client application) to the Elsa broker. It is **not** the upstream OIDC client registration: that upstream registration belongs in the connection's `adapterSettings` and `secretBindings`.

Each client has exact, deployment-controlled registrations:

* `clientId` and `displayName`.
* `clientType`: `confidential` for a server-side client or `public` for a browser/WASM client.
* `callbackUris`: exact broker completion-code destinations.
* `logoutCallbackUris`: exact post-logout destinations.
* `allowedReturnPathPrefixes`: safe application-relative paths such as `/` or `/workflows`.
* `allowedOrigins`: required for public/WASM clients, for example `https://studio.example.com`.
* A `secretBinding` for confidential clients. Public clients never contain a secret.
* `isEnabled`.

For a WebAssembly Studio client, use a public registration and register its exact browser origin:

```json
{
  "clientId": "elsa-studio-wasm",
  "displayName": "Elsa Studio WebAssembly",
  "clientType": "public",
  "callbackUris": ["https://studio.example.com/authentication/external/callback"],
  "logoutCallbackUris": ["https://studio.example.com/authentication/external/logout-callback"],
  "allowedOrigins": ["https://studio.example.com"],
  "allowedReturnPathPrefixes": ["/"],
  "isEnabled": true
}
```

The broker always requires S256 PKCE. A confidential client also authenticates when exchanging the broker completion code; a public client uses PKCE and must never hold a client secret.

## Callbacks and provider registration

`Redirects:ExternalCallbackBaseUri` is the public, deployment-owned base URI from which Elsa derives upstream provider callbacks. It must be an absolute HTTPS URL in production. Do not try to configure callbacks per connection.

For the example above, register these exact URLs at the upstream provider:

```
https://elsa.example.com/elsa/api/external-authentication/callback/contoso-workforce
https://elsa.example.com/elsa/api/external-authentication/previews/callback/contoso-workforce-configuration
```

The normal callback uses the immutable logical connection **key**. The preview callback uses the stable connection **ID**. Register the preview callback only when administrators will use connection preview. Elsa's callback routes, correlation state, S256 PKCE, and validation cannot be overridden by a connection.

## OIDC adapter settings

The `openid-connect` adapter uses an authorization-code flow. Its safe default is discovery mode:

* `mode`: `discovery` or `manual`; use `discovery` whenever possible.
* `discoveryUrl`: exact HTTPS discovery-document URL in discovery mode.
* `clientId`: the Elsa Server registration at the upstream provider.
* `clientAuthenticationMethod`: `client_secret_basic` (default) or `client_secret_post`.
* `scopes`: optional requested scopes. `openid` is always included.
* `clientSecret`: a required secret binding field, never a value inside `adapterSettings`.
* `endSessionEndpoint`: optional explicit upstream logout endpoint.

Manual trust additionally requires `issuer`, `authorizationEndpoint`, and `tokenEndpoint`, plus either `jwksUri` or pinned `signingKeys`. Treat it as an exception: explicit trust overrides require both deployment allowance and the `external-authentication:provider-trust:unsafe` permission, plus an explicit confirmation when saved. Discovery-derived issuer, endpoints, and signing keys are the recommended configuration.

The adapter validates the authorization response and ID token, including correlation state, issuer, signature, audience/authorized party, expiry, nonce, and S256 PKCE. It projects only allowlisted claims and does not return provider tokens through broker or management APIs.

## Identity resolution, claims, and permissions

External claims do not automatically become Elsa permissions.

### Unlinked identities

The default unlinked identity policy is `reject`: an identity that has no link to an Elsa user cannot sign in. This is the safest starting point.

`create-user` can provision an Elsa user on first sign-in. For example, `defaultRoleIds` assigns existing Elsa roles only when a new user is created:

```json
{
  "type": "create-user",
  "settingsVersion": 1,
  "settings": {
    "defaultRoleIds": ["workflow-user"]
  }
}
```

The deployment controls which policies are selectable with `AllowedUnlinkedIdentityPolicyTypes`; it can also prevent database-owned connections from overriding `UnlinkedIdentityPolicy:DefaultType`. A matcher-based policy can select a deployed external-user matcher, but matchers do not assign roles or permissions.

### Claim projection and grant sources

`claimProjection` is an allowlist and size boundary for the claims retained after upstream validation. Configure only the claims required by identity matching or permission mapping. Use `redactedClaimTypes` to prevent sensitive values from appearing in management/preview output.

The built-in grant-source allowlist contains `elsa-roles`, `claim-mapping`, `group-mapping`, and `claim-pass-through`. `AllowedPermissionGrantSourceTypes` controls which deployed sources a connection may select. `PermissionGrants:AllowedPermissions` and `PermissionGrants:DeniedPermissions` are deployment-wide final boundaries, applied after grant sources calculate candidate permissions.

Keep `claim-pass-through` tightly bounded. Explicit mappings and Elsa roles are easier to audit than passing provider claim values through as Elsa permission names.

## Connection ownership and overrides

`Connections` creates configuration-owned, immutable connections. They are ideal when operators deploy a known identity provider configuration and want it reviewed through source-controlled deployment configuration.

When `EnableDatabaseConnections` is `true` (the default), authorized administrators can create database-owned connections through Studio or the management API. The connection source rules are:

* A configuration-owned connection takes precedence when it has the same effective key and scope as a database connection.
* Studio marks the database connection as **shadowed**; it does not silently overwrite configuration.
* With `AllowConfigurationConnectionOverrides: true`, an administrator can create or promote a complete database-owned override. The override preserves its own lifecycle and secret bindings.
* A disabled override still shadows the configuration baseline. Archiving the override reveals the configuration connection; restoring it resumes shadowing in a disabled state.
* The final-login-path guard rejects a change that would remove the last normal sign-in option without an approved recovery method or privileged override.

Configuration-owned rows are inspect-only. Database-owned mutations use optimistic concurrency: read the ETag and send it as `If-Match` when updating, enabling, disabling, archiving, restoring, or changing secret bindings.

## Operational configuration

The broker has secure defaults that are suitable for most deployments:

* Local Elsa username/password login is enabled as a normal chooser option; set `LocalLogin:IsEnabled` to `false` to remove it.
* Provider HTTPS is required, private/loopback/link-local destinations are denied, and redirects are revalidated (maximum three by default).
* Broker transactions last 10 minutes; completion codes last one minute; preview state lasts 10 minutes; external sessions are bounded to eight hours.
* Anonymous discovery/initiation/callback/token operations use named rate-limit policies.
* Upstream logout is disabled unless a connection enables it.
* Session administration is enabled. The ASP.NET Core health-check bridge is opt-in via `AddExternalAuthenticationHealthCheck()` and is tagged `external-authentication` and `optional`.

Tune `ProviderEgress`, `Lifetimes`, `RateLimits`, `Logout`, `FinalLoginPathGuard`, and `Operations` only with a clear operational requirement. See [Production](/guides/authentication/external-authentication/production) for clustered hosts, key sharing, egress restrictions, and safe secret rotation.


# Keycloak Walkthrough

A concrete end-to-end walkthrough for connecting Keycloak to Elsa External Authentication and signing in from Elsa Studio.

{% hint style="warning" %}
This walkthrough uses the Elsa 3.8 preview packages from Feedz. External Authentication is under active development. Use sample credentials only in an isolated development environment.
{% endhint %}

This walkthrough makes the provider-neutral configuration concrete with Keycloak. The same Elsa configuration model applies to Microsoft Entra ID, Auth0, Okta, OpenIddict, and other conforming OpenID Connect providers.

Before starting, complete [installation](/guides/authentication/external-authentication/installation) and choose a [Studio host model](/guides/authentication/external-authentication/studio-integration).

## Values Used in This Walkthrough

| Setting                                     | Example                             |
| ------------------------------------------- | ----------------------------------- |
| Elsa API base URL                           | `https://elsa.example/elsa/api/`    |
| Studio URL                                  | `https://studio.example`            |
| Keycloak issuer                             | `https://login.example/realms/elsa` |
| Connection key                              | `keycloak`                          |
| Provider client ID                          | `elsa-server-broker`                |
| Studio Server Authentication Client ID      | `elsa-studio-server`                |
| Studio WebAssembly Authentication Client ID | `elsa-studio-wasm`                  |

Replace every example hostname and secret before running the configuration.

## 1. Create the Keycloak Realm and User

In Keycloak:

1. Create or select a realm, such as `elsa`.
2. Create a test user and set a password.
3. Ensure the account may complete the standard authorization-code flow.
4. Add any provider claims you intend to project or use for authorization, such as `email`, `preferred_username`, or a realm role.

External Authentication validates the provider identity and then resolves it to an Elsa User. A Keycloak role does not automatically grant an Elsa permission.

## 2. Register Elsa Server at Keycloak

Create a confidential OpenID Connect client for Elsa Server:

* Enable the standard authorization-code flow.
* Disable implicit flow.
* Require client authentication.
* Use `client_secret_basic` when available; `client_secret_post` is also supported.
* Register the exact normal callback URI:

```
https://elsa.example/elsa/api/external-authentication/callback/keycloak
```

If administrators will run interactive connection previews, also register the preview callback URI shown by the connection detail screen. It uses the connection record ID and is intentionally different from the normal callback.

Do not register a Studio callback at Keycloak. Keycloak redirects to Elsa Server; Elsa later redirects to the registered Studio Authentication Client.

Copy the generated provider client secret into a deployment secret store. Do not put it in `adapterSettings`, source control, a browser application, or a Studio connection document.

## 3. Configure the Provider Connection

The following example creates a deployment-owned connection. In a conventional host, place it under the root `ExternalAuthentication` section. In Modular Server, place the same object inside the `ExternalAuthentication` feature configuration.

```json
{
  "ExternalAuthentication": {
    "LocalLogin": {
      "IsEnabled": true
    },
    "Redirects": {
      "ExternalCallbackBaseUri": "https://elsa.example/elsa/api/"
    },
    "ProviderEgress": {
      "RequireHttps": true,
      "AllowPrivateNetworkDestinations": false,
      "AllowedHosts": ["login.example"]
    },
    "Connections": [
      {
        "Id": "keycloak-configuration",
        "Key": "keycloak",
        "AdapterType": "openid-connect",
        "AdapterSettingsVersion": 2,
        "AdapterSettings": {
          "mode": "discovery",
          "discoveryUrl": "https://login.example/realms/elsa/.well-known/openid-configuration",
          "clientId": "elsa-server-broker",
          "clientAuthenticationMethod": "client_secret_basic",
          "scopes": ["profile", "email"]
        },
        "SecretBindings": {
          "clientSecret": {
            "Ownership": "External",
            "ResolverType": "configuration",
            "Reference": "ExternalAuthentication:Secrets:KeycloakClientSecret",
            "ExpectedType": "text",
            "ExpectedScope": "external-authentication"
          }
        },
        "DisplayName": "Keycloak",
        "DisplayOrder": 10,
        "IsPreferred": true,
        "IsEnabled": true,
        "UnlinkedPolicy": {
          "Type": "create-user",
          "SettingsVersion": 1,
          "Settings": {
            "defaultRoleIds": ["admin"]
          }
        },
        "ClaimProjection": {
          "AllowedClaimTypes": [
            "preferred_username",
            "name",
            "given_name",
            "family_name",
            "email"
          ],
          "RedactedClaimTypes": [],
          "MaximumClaimCount": 64,
          "MaximumValueLength": 1024,
          "MaximumTotalBytes": 16384
        },
        "UpstreamLogoutMode": "UserChoice"
      }
    ]
  }
}
```

Supply the referenced value through deployment configuration, for example an environment variable named `ExternalAuthentication__Secrets__KeycloakClientSecret`. Do not add the value to the JSON file.

{% hint style="danger" %}
Assigning the `admin` role on just-in-time creation is convenient for an isolated walkthrough, but it grants full Elsa access when that role contains `*`. Use [least-privilege roles and explicit permission grants](/guides/authentication/external-authentication/production#authorization-and-administrative-safety) in a real environment.
{% endhint %}

`BindExternalAuthenticationOptions` preserves the adapter's versioned JSON settings. Do not replace it with a plain options bind in a conventional host.

## 4. Register Studio as an Elsa Authentication Client

For Studio Server, add a confidential Authentication Client:

```json
{
  "ClientId": "elsa-studio-server",
  "DisplayName": "Elsa Studio Server",
  "ClientType": "Confidential",
  "CallbackUris": [
    "https://studio.example/authentication/external/callback"
  ],
  "LogoutCallbackUris": [
    "https://studio.example/authentication/external/logout-callback"
  ],
  "AllowedReturnPathPrefixes": ["/"],
  "SecretBinding": {
    "Ownership": "External",
    "ResolverType": "configuration",
    "Reference": "ExternalAuthentication:Secrets:StudioServerClientSecret"
  },
  "IsEnabled": true
}
```

For Studio WebAssembly, use a public client, omit the secret, and register the exact browser origin:

```json
{
  "ClientId": "elsa-studio-wasm",
  "DisplayName": "Elsa Studio WebAssembly",
  "ClientType": "Public",
  "CallbackUris": [
    "https://studio.example/authentication/external/callback"
  ],
  "LogoutCallbackUris": [
    "https://studio.example/authentication/external/logout-callback"
  ],
  "AllowedOrigins": ["https://studio.example"],
  "AllowedReturnPathPrefixes": ["/"],
  "IsEnabled": true
}
```

If both Studio hosts are deployed, give each its own Authentication Client ID and exact URLs.

## 5. Configure Studio

Set `Authentication:Provider` to `ExternalAuthentication` and choose the matching host configuration from [Studio Integration](/guides/authentication/external-authentication/studio-integration). Do not register direct OIDC and brokered External Authentication in the same Studio host.

Restart Elsa Server and Studio after changing deployment-owned configuration.

## 6. Validate the Connection

In Studio:

1. Open **Administration → Identity & access → Identity provider connections**.
2. Open the `Keycloak` configuration-owned connection.
3. Confirm the displayed callback URI exactly matches the Keycloak client registration.
4. Run **Validate** to check local structure and secret-binding state.
5. Run **Test connection** to fetch discovery metadata and signing keys.
6. Run **Preview sign-in** to verify the provider identity without creating a user, link, credential, or normal session.

Configuration-owned connections are read-only in Studio. If deployment policy permits overrides, Studio can create a complete database-owned override; it never partially merges settings or copies secret values.

## 7. Sign In

1. Sign out of Studio and open `/login`.
2. Select **Keycloak**. A preferred connection may be emphasized or sorted first, but Studio never auto-starts it.
3. Authenticate at Keycloak.
4. Confirm Elsa returns to Studio and creates or resolves the expected Elsa User.
5. Verify a normal session appears under **Administration → Identity & access → Authentication sessions** when session administration is enabled.
6. Verify the user can perform only the Elsa operations allowed by their Elsa roles and permissions.

Keep local login enabled until this complete path and a recovery account have been tested.

## Provider Variations

For Microsoft Entra ID, Auth0, Okta, and other providers, change the discovery URL, provider client registration, allowed egress host, scopes, and claim selection. The Elsa callback shape, Authentication Client distinction, PKCE completion flow, identity linking, Elsa-issued credentials, and explicit authorization model remain the same.

## Next Steps

* [Configuration Reference](/guides/authentication/external-authentication/configuration)
* [Studio Integration](/guides/authentication/external-authentication/studio-integration)
* [Administration in Studio](/guides/authentication/external-authentication/administration)
* [Production and Security](/guides/authentication/external-authentication/production)
* [Troubleshooting](/guides/authentication/external-authentication/troubleshooting)


# Studio Integration

Configure Elsa Studio as an Elsa External Authentication broker client in Elsa 3.8 preview.

> **Preview feature — Elsa 3.8.** External Authentication is new in Elsa 3.8 and is currently available only from the Feedz.io preview feed while the feature is under development. Contracts, package names, and screens can change before the stable release. Validate the exact preview versions of Elsa Core and Elsa Studio together before using this in production.

External Authentication makes Elsa Server the authentication broker for Studio. Studio asks Elsa Server which sign-in methods are available, starts the selected connection, and exchanges the resulting one-time completion code for an Elsa credential. This differs from Studio's direct OpenID Connect integration: Studio no longer talks directly to each upstream identity provider.

Use this mode when Elsa should centrally manage the available upstream connections at runtime. It is an opt-in Studio mode; it does not change the existing direct `OpenIdConnect` or `ElsaIdentity` modes.

<figure><img src="/files/4LTTGhE4JGwPDl9MKpqe" alt="Elsa Studio Blazor Server login chooser showing local and external sign-in methods"><figcaption><p>The broker-backed login chooser in the Blazor Server host. A preferred method is highlighted but is never started automatically.</p></figcaption></figure>

## Before you start

Complete the Elsa Server External Authentication setup first:

1. Install compatible Elsa 3.8 preview Core and Studio packages from the Feedz.io preview feed.
2. Enable and configure External Authentication in Elsa Server, including at least one enabled, valid connection or a broker-local sign-in path.
3. Create a dedicated **Elsa Authentication Client** for Studio. This is not an Elsa API Application and grants no permissions.
4. Register Studio's exact sign-in and logout callback URLs, allowed local return paths, and—for a WebAssembly client—the exact Studio origin.
5. Ensure the signed-in user receives Elsa `permissions` claims needed for Studio and any management actions they will perform.

Do not use this guide to configure an upstream provider directly in Studio. Configure its discovery, trust, client credentials, scopes, policy, and claim handling on the Elsa Server connection instead.

## Packages and host selection

Add the shared management/UI package plus exactly one package matching the Studio hosting model:

```bash
dotnet add package Elsa.Studio.ExternalAuthentication --prerelease
# Choose one host package:
dotnet add package Elsa.Studio.ExternalAuthentication.BlazorServer --prerelease
# or
dotnet add package Elsa.Studio.ExternalAuthentication.BlazorWasm --prerelease
```

{% hint style="warning" %}
Core and Studio previews are published independently. A new Core preview can appear before the Studio preview that contains these modules. Verify that all three Studio package IDs resolve before changing a host:

```bash
dotnet package search Elsa.Studio.ExternalAuthentication \
  --source https://f.feedz.io/elsa-workflows/elsa-3/nuget/index.json \
  --prerelease
```

If the search returns no packages, the Studio modules are not yet available in the current Feedz snapshot. Wait for a later Elsa Studio 3.8 preview or build the matching `elsa-studio` 3.8 source; do not substitute the direct-OIDC packages.
{% endhint %}

Keep all Elsa Core packages on one Core preview build and all Elsa Studio packages on one Studio preview build. Their numeric preview suffixes are independent, so they do not need to be identical. Pin both tested versions for repeatable restores and validate them together.

| Hosting model      | Packages                                                                                | Broker client type    |
| ------------------ | --------------------------------------------------------------------------------------- | --------------------- |
| Blazor Server      | `Elsa.Studio.ExternalAuthentication`, `Elsa.Studio.ExternalAuthentication.BlazorServer` | Confidential          |
| Blazor WebAssembly | `Elsa.Studio.ExternalAuthentication`, `Elsa.Studio.ExternalAuthentication.BlazorWasm`   | Public, PKCE required |

The shared package contributes the login methods and security-management UI. The host package sets the credential boundary, authentication state provider, API handler, SignalR token configuration, and sign-out entry point.

The WebAssembly package also requires this script in the host page (normally `wwwroot/index.html`):

```html
<script src="_content/Elsa.Studio.ExternalAuthentication.BlazorWasm/external-authentication.js"></script>
```

The script provides the browser Web Crypto and browser-storage functions used for PKCE and tokens. Place it before the Blazor WebAssembly bootstrap script.

## Select the authentication mode

Set the Studio host configuration value below. A host must select one provider: `ExternalAuthentication`, `OpenIdConnect`, `ElsaIdentity`, or legacy `ElsaLogin`.

```json
{
  "Authentication": {
    "Provider": "ExternalAuthentication"
  }
}
```

Do not mix direct OIDC and broker registrations in the same host. Startup validation rejects combinations of legacy login, Elsa Identity, direct OIDC, and broker registrations that would select different trust models.

The management module can be registered in a host regardless of the selected provider, but its menu is shown only when the Elsa Server External Authentication feature is available. Broker sign-in becomes active only when `Authentication:Provider` is `ExternalAuthentication`.

## Register services

Register the host-specific broker service when the selected provider is `ExternalAuthentication`, and set the broker handler as the remote backend authentication handler. Then register the shared module after creating the remote-backend configuration.

### Blazor Server

```csharp
using Elsa.Studio.ExternalAuthentication.BlazorServer.Extensions;
using Elsa.Studio.ExternalAuthentication.BlazorServer.HttpMessageHandlers;
using Elsa.Studio.ExternalAuthentication.Extensions;

// When Authentication:Provider is ExternalAuthentication.
builder.Services.AddExternalAuthenticationBroker(options =>
    builder.Configuration
        .GetSection("Authentication:ExternalAuthentication")
        .Bind(options));

var backendApiConfig = new BackendApiConfig
{
    ConfigureBackendOptions = options =>
        builder.Configuration.GetSection("Backend").Bind(options),
    ConfigureHttpClientBuilder = options =>
        options.AuthenticationHandler =
            typeof(ExternalAuthenticationAuthenticatingApiHttpMessageHandler)
};

builder.Services.AddRemoteBackend(backendApiConfig);
builder.Services.AddExternalAuthenticationModule(backendApiConfig);
```

The host must also use the normal ASP.NET Core authentication and authorization middleware and map controllers. The broker package supplies the Server callback controller:

```csharp
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
```

### Blazor WebAssembly

```csharp
using Elsa.Studio.ExternalAuthentication.BlazorWasm.Extensions;
using Elsa.Studio.ExternalAuthentication.BlazorWasm.HttpMessageHandlers;
using Elsa.Studio.ExternalAuthentication.Extensions;

// When Authentication:Provider is ExternalAuthentication.
builder.Services.AddExternalAuthenticationBroker(options =>
    builder.Configuration
        .GetSection("Authentication:ExternalAuthentication")
        .Bind(options));

var backendApiConfig = new BackendApiConfig
{
    ConfigureBackendOptions = options =>
        builder.Configuration.GetSection("Backend").Bind(options),
    ConfigureHttpClientBuilder = options =>
        options.AuthenticationHandler =
            typeof(ExternalAuthenticationAuthenticatingApiHttpMessageHandler)
};

builder.Services.AddRemoteBackend(backendApiConfig);
builder.Services.AddExternalAuthenticationModule(backendApiConfig);
```

The package registers the callback pages at the fixed client-local paths. Do not add a competing callback route.

## Configuration reference

All settings below bind from `Authentication:ExternalAuthentication`.

| Setting              | Server   | WASM      | Notes                                                         |
| -------------------- | -------- | --------- | ------------------------------------------------------------- |
| `ClientId`           | Required | Required  | The deployment-managed Elsa Authentication Client identifier. |
| `ClientSecret`       | Required | Forbidden | A WASM public client must never contain a client secret.      |
| `CallbackPath`       | Fixed    | Fixed     | Must be `/authentication/external/callback`.                  |
| `LogoutCallbackPath` | Fixed    | Fixed     | Must be `/authentication/external/logout-callback`.           |
| `BrowserStorage`     | N/A      | Optional  | `Memory`, `Session`, or `Durable`; default is `Memory`.       |

Custom callback paths are rejected at startup. Register the **absolute** values formed from the public Studio URL and these paths with the Elsa Authentication Client. For example, if the Studio public URL is `https://studio.example.com`, register:

```
https://studio.example.com/authentication/external/callback
https://studio.example.com/authentication/external/logout-callback
```

If Studio is behind a reverse proxy, make sure the request scheme and host seen by the Server host are the public values. Otherwise it will construct a callback URI that does not match the URL registered with Elsa Server.

### Blazor Server example

Store the secret in a secret provider, environment variable, or deployment configuration—not in source control.

```json
{
  "Backend": {
    "Url": "https://elsa.example.com/elsa/api"
  },
  "Authentication": {
    "Provider": "ExternalAuthentication",
    "ExternalAuthentication": {
      "ClientId": "elsa-studio-server",
      "ClientSecret": "<resolved by deployment secret configuration>",
      "CallbackPath": "/authentication/external/callback",
      "LogoutCallbackPath": "/authentication/external/logout-callback"
    }
  }
}
```

Server uses a confidential client. It exchanges the authorization code on the server and keeps the access/refresh credentials in a server-side ticket store. The browser receives an `ElsaStudio.ExternalAuthentication` cookie that is secure, HTTP-only, `SameSite=Lax`, non-sliding, and has an eight-hour lifetime. The refresh credential is never exposed to Blazor components or browser code.

### Blazor WebAssembly example

```json
{
  "Backend": {
    "Url": "https://elsa.example.com/elsa/api"
  },
  "Authentication": {
    "Provider": "ExternalAuthentication",
    "ExternalAuthentication": {
      "ClientId": "elsa-studio-wasm",
      "CallbackPath": "/authentication/external/callback",
      "LogoutCallbackPath": "/authentication/external/logout-callback",
      "BrowserStorage": "Memory"
    }
  }
}
```

WASM is a public client. It creates S256 PKCE verifier/state values in the browser, validates the exact callback origin/path, exchanges the one-time code with the broker, and attaches the Elsa access token to backend API and SignalR connections.

<figure><img src="/files/5DYbD4p1ZxandZm8Y5R6" alt="Elsa Studio WebAssembly login chooser showing local and external sign-in methods"><figcaption><p>The same broker-backed chooser in the WebAssembly host. The hosting models offer the same user choice while using different credential boundaries.</p></figcaption></figure>

### CORS for WebAssembly

The Authentication Client's `AllowedOrigins` setting validates which browser origin may participate in the broker flow. It does **not** configure ASP.NET Core CORS for Elsa API requests. For a WebAssembly deployment, register the exact public Studio origin in both places:

1. Add the origin to the public Elsa Authentication Client's `AllowedOrigins` collection.
2. Configure an ASP.NET Core CORS policy on the Elsa Server host that permits that origin, the API methods and headers Studio uses, and WebSocket/SignalR access where applicable.
3. Apply the policy before authentication, authorization, and endpoint mapping.

Do not combine credentialed requests with `AllowAnyOrigin`. Keep development ports in sync too: `https://localhost:7052` and `https://studio.example.com` are different origins.

### `BrowserStorage` decision

| Value     | Token lifetime in the browser | Use when                                      | Trade-off                                                                 |
| --------- | ----------------------------- | --------------------------------------------- | ------------------------------------------------------------------------- |
| `Memory`  | Current running app only      | Default/recommended setting                   | User signs in again after reload, a new tab, or tab close.                |
| `Session` | Current browser tab           | Reload continuity is needed                   | Browser script compromise can access tokens; close the tab when finished. |
| `Durable` | Local storage beyond session  | Persistent sign-in is an explicit requirement | Largest exposure to browser script compromise and shared-device use.      |

`Session` and `Durable` produce a startup security warning. Use them only after assessing XSS controls, device sharing, and the impact of a stolen browser token. Neither option turns a public WASM client into a confidential client.

## Sign-in and sign-out sequence

1. Studio loads `/login` and anonymously requests the available methods for its `ClientId` from Elsa Server.
2. Elsa Server returns presentation-only local and/or external methods plus an optional preferred method key.
3. The user explicitly chooses an external provider or enters broker-local credentials. A preferred method is highlighted only; Studio never starts it automatically.
4. Studio starts an authorization-code request using `response_type=code` and S256 PKCE. Server hosts retain PKCE state/verifier server-side; WASM retains a short-lived browser transaction.
5. The upstream/provider flow returns an opaque completion code to the fixed Studio callback.
6. Studio consumes callback state once, exchanges the code at Elsa Server, and sends the user only to a validated local return path. Invalid, external, backslash, or `//` return paths become `/`.
7. API calls and SignalR receive the current Elsa access token through the registered authentication handler.

On sign-out, Studio supports `local` and `upstream` modes. Local sign-out is always completed even if Elsa Server or the upstream provider cannot be reached. An upstream continuation is accepted only when it is an Elsa Server same-origin logout route, then returns to the fixed logout callback.

## Moving from direct OIDC

`OpenIdConnect` and `ExternalAuthentication` are separate deployment modes. Plan a controlled change:

1. Keep `Authentication:Provider` set to `OpenIdConnect`.
2. Configure and test the Elsa broker connection and a separate Studio Authentication Client.
3. Keep direct and broker client secrets in their existing deployment-owned secret stores; no Studio screen or migration copies a secret.
4. Change the provider value to `ExternalAuthentication` and restart Studio.
5. Verify sign-in, refresh, backend authorization, SignalR, and both logout modes.

To roll back, restore `Authentication:Provider` to `OpenIdConnect` and restart Studio. The broker does not change the retained direct OIDC settings.

## Verify the integration

After deployment, confirm all of the following:

* `/login` shows only the methods Elsa Server exposes to the Studio client.
* Selecting a provider opens the expected Elsa-managed authorization flow; it does not automatically launch merely because it is preferred.
* Callback and logout callback match the registered public URLs exactly.
* A signed-in user can load Studio and call an authorized Elsa API.
* A user without a required `permissions` claim receives the expected API authorization result rather than a misleading sign-in success.
* Server credentials do not appear in browser storage, URLs, page markup, or logs. For WASM, verify the configured storage behavior deliberately.

For management, testing, and operational verification, see [External Authentication administration](/guides/authentication/external-authentication/administration). For failures and diagnostic steps, see [Troubleshooting](/guides/authentication/external-authentication/troubleshooting).


# Administration

Manage Elsa External Authentication connections, identity links, and sessions from Elsa Studio 3.8 preview.

> **Preview feature — Elsa 3.8.** This functionality is available from the Feedz.io preview feed while it remains under development. Verify matching Elsa Core and Studio preview versions before relying on a management action.

After a Studio host registers `Elsa.Studio.ExternalAuthentication`, its **Administration → Identity & access** menu can expose an Elsa Server-backed administration experience. The management UI is feature-gated: it appears when Elsa Server advertises `Elsa.ExternalAuthentication.ShellFeatures.ExternalAuthentication`. It is also permission-gated; hidden controls do not replace Elsa API authorization.

<figure><img src="/files/W7rVV0m7l3hEWLF14d9I" alt="Identity provider connections page in Elsa Studio"><figcaption><p>Identity provider connections under Administration and Identity &#x26; access, including deployment-owned connections and their current diagnostic state.</p></figcaption></figure>

## Routes and permissions

| Route                                              | Purpose                                          | Menu permission                            |
| -------------------------------------------------- | ------------------------------------------------ | ------------------------------------------ |
| `/security/external-authentication/connections`    | Manage identity-provider connections             | `external-authentication:connections:read` |
| `/security/external-authentication/identity-links` | Prelink, replace, and unlink external identities | `external-authentication:links:manage`     |
| `/security/external-authentication/sessions`       | View and optionally revoke broker sessions       | `external-authentication:sessions:read`    |

Connection actions additionally use these permission strings:

| Action                                        | Permission                                                  |
| --------------------------------------------- | ----------------------------------------------------------- |
| Create connection or an allowed override      | `external-authentication:connections:create`                |
| Save, enable, disable, or promote an override | `external-authentication:connections:update`                |
| Archive or restore                            | `external-authentication:connections:archive`               |
| Test connection                               | `external-authentication:connections:test`                  |
| Preview sign-in                               | `external-authentication:connections:preview`               |
| Configure unlinked-identity policy            | `external-authentication:policies:manage`                   |
| Delegate connection permissions               | `external-authentication:permissions:delegate`              |
| Use unrestricted delegation                   | `external-authentication:permissions:delegate-unrestricted` |
| Configure unsafe provider settings            | `external-authentication:provider-trust:unsafe`             |
| Load roles for a create-user policy           | `read:role`                                                 |
| Revoke a session                              | `external-authentication:sessions:revoke`                   |

`*` satisfies the Studio permission affordance checks, but grant only the smallest set required. The Elsa Server endpoints remain authoritative: a screen can be visible while a request is still denied by server-side policy.

## Connection management

Open **Administration > Identity & access > Identity provider connections**. The list supports searching, filtering by source (`Database` or `Configuration`), showing archived records, and cursor paging. It displays each connection's key, adapter, ownership, availability, latest test observation, and whether it is preferred.

An enabled, valid database connection becomes a login method without restarting Studio or Elsa Server. A connection listed as configuration-owned is supplied by deployment configuration and has different edit rules.

### Create a database connection

1. Select **Create connection**.
2. Enter the immutable **Connection key**, display name, order, icon, adapter type, and provider settings shown by the server-provided adapter descriptor.
3. Supply required provider fields and write-only managed secrets where the server supports them.
4. Choose an unlinked-identity policy when required. Descriptor fields are rendered by Studio rather than entered as arbitrary JSON.
5. Save the draft, then use **Test connection**. Correct validation errors and repeat the test until the connection is valid.
6. Use **Preview sign-in** before enabling it when you need to validate the end-user identity/policy outcome.
7. Enable the connection only when it is ready to be discoverable at login.

<figure><img src="/files/Et1sHoLw6MbcGlLfhZJQ" alt="Create identity provider connection form in Elsa Studio"><figcaption><p>The database-connection editor starts with general metadata; adapter-specific provider settings and policies appear in their own tabs.</p></figcaption></figure>

Provider callback URLs are derived by Elsa Server from deployment-owned public origin configuration. Studio displays them read-only; register the displayed value with the upstream provider rather than attempting to edit it in Studio.

<figure><img src="/files/3MtNKv7JQW76ISk54Xyz" alt="Read-only configuration-owned Keycloak connection in Elsa Studio"><figcaption><p>A configuration-owned Keycloak connection. Its normal and preview callback URIs are deployment-derived and displayed read-only.</p></figcaption></figure>

### Configuration-owned connections and overrides

Configuration-owned connections remain visible but read-only. Their provider and secret configuration belongs to the deployment.

When both conditions are true, Studio offers **Create full Database override**:

1. The actor has the connection create permission.
2. Elsa Server advertises that a configuration override is allowed for the connection (`canCreateOverride`).

Creating the override produces an explicit complete database record. Secret bindings are cleared: they are never cloned from configuration, so an administrator must deliberately reconfigure required secrets before enabling the override. A database override can shadow the deployment connection even while disabled. Archiving the override reveals the configuration connection again.

Do not treat an override as a way to export deployment configuration. It is a new record with a deliberate configuration boundary.

### Secrets

Secret fields are write-only. When Elsa Server advertises an installed managed secret resolver, Studio lets an authorized user choose the resolver and submit a replacement value. The value goes only to the managed-secret replacement endpoint; Studio clears its local input and the response exposes only ownership, configured state, and resolvability—not the value.

* Deployment-managed resolver/reference bindings are neither displayed nor editable in Studio.
* If the server has no managed resolver, Studio hides the managed editor and explains why.
* A required managed secret cannot be removed while the connection is enabled. Disable the connection first, then remove or replace the binding.
* Never paste a secret into a ticket, browser console capture, exported screen shot, or source-controlled configuration.

### Provider settings, policies, and roles

Adapters supply field descriptors, validation, capabilities, and (optionally) a custom editor contract. Studio uses these server-provided descriptors for provider-specific fields, so the available fields vary by adapter and preview version.

For unlinked identities, the policy form is descriptor-driven:

* `match-user` is shown only when Elsa Server advertises an installed user matcher. Studio renders that matcher's fields and required claim types.
* Create-user outcomes load available roles from `/identity/roles`.
* Loading roles requires `read:role`. If roles cannot be loaded, the role picker becomes read-only and warns the operator instead of accepting raw role IDs.

Although the contract contains permission and claim mapping DTOs, this preview does not expose customer-facing mapping or permission-preview editors. Do not document those as an available Studio workflow.

### Test connection and Preview sign-in

**Test connection** performs an on-demand server-side diagnostic and records a redacted observation with status, category, summary, warnings, duration, and correlation ID. It never returns provider access tokens.

<figure><img src="/files/jcJ8zpheJIHS51MYcUKF" alt="Successful identity provider connection test in Elsa Studio"><figcaption><p>A successful live metadata test. The diagnostic result is deliberately redacted and includes a correlation ID for server-side investigation.</p></figcaption></figure>

**Preview sign-in** verifies a connection's effective current revision in a separate tab. It returns a one-time, redacted result such as issuer, masked subject, policy decision, projected safe claims, proposed action, and warnings. It does **not** create a user, identity link, credential, or normal session.

<figure><img src="/files/GxF9TOuFjVzGpx648Oqs" alt="Preview sign-in ready state in Elsa Studio"><figcaption><p>Preview sign-in uses a separate tab and a one-time result. It exercises the effective connection without creating a normal session.</p></figcaption></figure>

Use the preview when validating a newly configured provider or policy. It is not a substitute for testing the complete Studio login flow with a non-admin test account.

### Enable, disable, archive, and restore

* **Enable** exposes an enabled, valid database connection through login discovery.
* **Disable** stops normal discovery without deleting the record. Existing external sessions can remain active until expiry or may be revoked when the operator has session-revocation permission.
* **Archive** removes the connection from login discovery but preserves identity links. It can be restored later.
* **Restore** restores the record as disabled; validate and explicitly enable it before it can be used for normal login.

Disabling the final normal login method is a recovery-sensitive action. Studio requires an explicit confirmation that an independent recovery path has been verified. Break-glass authentication remains outside normal login discovery; do not approve this override merely to remove a failing provider.

## Identity links

Open **Administration > Identity & access > External identity links** to associate an existing Elsa user with an external identity before first sign-in, replace the association, or remove it.

<figure><img src="/files/YNrJGmxFC3XBCYYHCKXW" alt="Create external identity link dialog in Elsa Studio"><figcaption><p>Prelink an exact upstream issuer and subject to an existing Elsa user before first sign-in.</p></figcaption></figure>

### Prelink an identity

1. Select **Create external identity link**.
2. Select the Elsa user and the connection key.
3. Enter the upstream issuer and subject exactly as supplied by the provider.
4. Create the link.

The operation sends `UserId`, `ConnectionKey`, `Issuer`, and `Subject` to Elsa Server. It avoids relying on a first-login matching outcome where an account must be explicitly associated in advance.

### Replace or unlink

* **Replace link** changes the identity association for an existing link; the UI warns that sign-in history is reset.
* **Unlink** removes the association after confirmation. The user can no longer sign in through that external identity unless another policy/link permits it.

Use tenant and user identifiers carefully. This page is intended for operators who understand the identity source; it is not a general user-profile editor.

## Session administration

**Administration > Identity & access > Authentication sessions** is optional and appears with `external-authentication:sessions:read`. It supports filters for user ID, connection key, and active/revoked status, plus cursor paging.

{% hint style="warning" %}
In the current 3.8 preview, Studio labels and sends the connection filter as **Connection ID**, while the Elsa Server endpoint expects `connectionKey`. Leave that Studio filter empty or query the REST API with `connectionKey` until the preview clients are aligned.
{% endhint %}

The page intentionally shows only safe metadata: session ID, user ID, tenant ID, connection key, start time, last refresh time, expiry, revocation time, and status. It never exposes tokens, external subjects, or claim snapshots.

<figure><img src="/files/DGHG35hyQQiU9GXyUUIc" alt="Authentication sessions page in Elsa Studio"><figcaption><p>The session administration page exposes operational metadata only; tokens and upstream identity values never appear.</p></figcaption></figure>

With `external-authentication:sessions:revoke`, select **Revoke** and confirm. The request uses the `administrator_revoked` reason; the user must authenticate again. Revocation cannot recover or reveal the session's tokens.

## Operational checklist

Before announcing a new connection to users:

* [ ] The connection is valid and has a current successful test observation.
* [ ] A preview sign-in gives the intended policy result and no unexplained warnings.
* [ ] Required managed secrets are configured/resolvable and not copied from a configuration-owned record.
* [ ] The upstream callback URI is the read-only URI Elsa Server reports.
* [ ] The intended normal and recovery login paths have been tested.
* [ ] Administrators have least-privilege connection, link, and session permissions.
* [ ] A rollback/recovery decision is ready before disabling the previous final login method.

For the Studio host setup, see [Studio integration](/guides/authentication/external-authentication/studio-integration). For errors, callback mismatches, authorization failures, and safe diagnostic steps, see [Troubleshooting](/guides/authentication/external-authentication/troubleshooting).


# Extensibility

Extend Elsa External Authentication with custom adapters, policies, matchers, permission grant sources, and Studio connection editors.

Elsa External Authentication is protocol-neutral. The 3.8 release supplies a built-in OpenID Connect adapter, but applications can add other protocol adapters and customize how unlinked identities, permissions, and connection settings are handled.

This page is for developers who own a trusted Elsa deployment and, optionally, an Elsa Studio package. It documents the extension contracts in the Elsa 3.8 preview release; the feature is still preview-only, so pin and test the exact package versions you deploy.

## Choose the right extension point

| Requirement                                                         | Contract                         | Runs where  |
| ------------------------------------------------------------------- | -------------------------------- | ----------- |
| Add a provider protocol such as SAML or a proprietary OAuth variant | `IExternalAuthenticationAdapter` | Elsa Server |
| Decide what happens when no external identity link exists           | `IUnlinkedIdentityPolicy`        | Elsa Server |
| Match an external identity to an existing Elsa user                 | `IExternalUserMatcher`           | Elsa Server |
| Convert trusted provider data into explicit Elsa grants             | `IPermissionGrantSource`         | Elsa Server |
| Replace the generic connection form for one adapter                 | `IConnectionCustomEditor`        | Elsa Studio |

An adapter owns the provider protocol. Policies, matchers, and grant sources should remain separate: a matcher answers *which user*, while a grant source answers *which Elsa permissions*. An upstream role or group never becomes an Elsa permission merely because it appears in a provider response.

## Register a server extension

Register the implementation with dependency injection and register its stable type with the External Authentication module. The second call does not create the implementation; it records the trusted extension type for startup validation and allowlist checks.

```csharp
using Elsa.ExternalAuthentication.Contracts;
using Elsa.ExternalAuthentication.Options;

services.AddExternalAuthenticationServices();

services.AddScoped<IExternalAuthenticationAdapter, ContosoAdapter>();
services.AddExternalAuthenticationExtension(
    ExternalAuthenticationExtensionKind.Adapter,
    ContosoAdapter.AdapterType);
```

Use the matching extension kind for the other server contracts:

```csharp
services.AddScoped<IExternalUserMatcher, ContosoUserMatcher>();
services.AddExternalAuthenticationExtension(
    ExternalAuthenticationExtensionKind.ExternalUserMatcher,
    ContosoUserMatcher.MatcherType);

services.AddScoped<IPermissionGrantSource, ContosoGrantSource>();
services.AddExternalAuthenticationExtension(
    ExternalAuthenticationExtensionKind.PermissionGrantSource,
    ContosoGrantSource.SourceType);
```

`AddExternalAuthenticationServices()` installs the protocol-neutral foundation and built-in in-memory stores. Configure the dedicated External Authentication persistence feature for durable or multi-node deployments; registering an extension does not make broker state durable.

### Restrict what the deployment can use

The registries reject duplicate types and can apply deployment allowlists. The option names are:

```json
{
  "ExternalAuthentication": {
    "AllowedAdapterTypes": ["openid-connect", "contoso"],
    "AllowedUnlinkedIdentityPolicyTypes": ["reject", "create-user"],
    "AllowedExternalUserMatcherTypes": ["contoso-directory"],
    "AllowedPermissionGrantSourceTypes": ["elsa-roles", "contoso-groups"]
  }
}
```

An allowlist entry must refer to an installed extension. An empty adapter or matcher allowlist permits every installed type; the built-in policy and grant source lists are non-empty by default. Selecting the built-in `match-user` policy requires at least one installed and allowed user matcher.

Treat these lists as a security boundary. Do not register an adapter, matcher, or grant source solely to make its descriptor visible if the deployment should not be able to select it.

## Implement a protocol adapter

`IExternalAuthenticationAdapter` has one stable `Type` and six responsibilities:

| Member                                 | Responsibility                                                                        |
| -------------------------------------- | ------------------------------------------------------------------------------------- |
| `Describe()`                           | Return the adapter settings schema, capabilities, and optional Studio editor contract |
| `ValidateAsync(...)`                   | Validate the effective connection and resolved secrets before use                     |
| `CreateAuthorizationRequestAsync(...)` | Build the upstream authorization redirect and protected transaction state             |
| `AuthenticateCallbackAsync(...)`       | Validate the callback and return the external identity and projected claims           |
| `TestAsync(...)`                       | Run an on-demand connection diagnostic without creating a login session               |
| `CreateLogoutRequestAsync(...)`        | Optionally create an upstream logout redirect                                         |

The built-in `OpenIdConnectExternalAuthenticationAdapter` is the reference implementation. It uses the context's resolved secrets, validates issuer, audience, signature, lifetime, nonce, and callback correlation, then returns an `ExternalIdentity` plus the claims permitted by the connection's projection. Keep provider tokens and client secrets inside the adapter boundary; do not place them in descriptors, Studio models, journal entries, or diagnostic results.

For an adapter that changes its settings shape, implement `IAdapterSettingsMigration` for each forward version step and register those migrations with DI:

```csharp
services.AddScoped<IAdapterSettingsMigration, ContosoSettingsV1ToV2>();
```

Elsa chains migrations from the stored version to the adapter's current `Describe().SettingsVersion`. A missing step, duplicate source version, invalid target version, or migration cycle fails rather than silently interpreting old settings.

## Implement policies, matchers, and grant sources

### Unlinked identity policies

Implement `IUnlinkedIdentityPolicy` when the deployment needs a decision other than the built-in `reject`, `create-user`, or `match-user` behavior. The policy returns one of these decisions:

* `Reject`, with a safe reason;
* `CreateUser`, with a `UserCreationProposal`; or
* `LinkExistingUser`, with the user ID and an authorization basis.

Do not make a policy trust an arbitrary client-supplied user ID. The policy receives the target tenant, effective connection, normalized external identity, projected claims, and policy settings.

### External user matchers

`IExternalUserMatcher` is the trusted lookup used by the built-in `match-user` policy. It receives the target tenant, effective connection, external identity, the required projected claims, and matcher settings. It must return either:

* `Match` with an Elsa user ID and a non-empty `AuthorizationBasis`; or
* `NoMatch`.

The `match-user` policy rejects if the matcher is missing, not allowed, has a settings-version mismatch, or returns an invalid result. If the matcher returns `NoMatch`, the policy's `noMatchAction` can reject or create a credential-less user with the configured default roles.

### Permission grant sources

Implement `IPermissionGrantSource` when provider information should produce explicit Elsa grants. Its `GetGrantsAsync` context includes the target tenant, user ID, effective connection, external identity, projected claims, and the selected source settings.

Return `PermissionGrant` values only for permissions the deployment intends to delegate. The deployment-level `PermissionGrants.AllowedPermissions` and `DeniedPermissions` boundaries still apply after source resolution. Keep the source mapping explicit and reviewable; do not pass through all upstream group names as permission names.

## Design the descriptor contract

Every server extension describes its settings using the same shape:

* stable type, display name, description, and positive settings version;
* zero or more `SettingFieldDescriptor` values;
* optional `CustomEditorContract` with a key and positive contract version.

The released validator rejects descriptors that violate these rules:

* extension and descriptor types must match;
* extension types use lowercase identifiers with optional `-` or `.` segments, such as `contoso-directory`;
* field names start with a lowercase letter and then use letters or digits;
* field names must be unique and have display text, a description, a supported value type, and a UI hint;
* supported value types are `string`, `secret`, `boolean`, `integer`, `number`, `uri`, `string-array`, and `json`;
* allowed values cannot be blank or duplicated;
* length and regular-expression validation must be valid; and
* a secret-binding field must use value type `secret` and set `IsRedacted`.

Use `VisibleWhen` for fields that depend on another field. The referenced field must exist, must not be the field being conditioned, and must have a non-empty expected value. Keep unsafe provider-trust fields marked `IsUnsafe`; Studio uses that metadata to require the corresponding permission before editing them.

Example descriptor shape:

```csharp
public ExternalAuthenticationAdapterDescriptor Describe() => new(
    AdapterType,
    "Contoso Directory",
    "Authenticates users with the Contoso directory protocol.",
    1,
    [
        new SettingFieldDescriptor(
            "endpoint",
            "Endpoint",
            "The HTTPS authorization endpoint.",
            "uri",
            true,
            "uri",
            null,
            [],
            new SettingFieldValidation(),
            false,
            false,
            null,
            null,
            false),
        new SettingFieldDescriptor(
            "clientSecret",
            "Client secret",
            "The secret used by the confidential provider client.",
            "secret",
            true,
            "secret",
            null,
            [],
            new SettingFieldValidation(),
            true,
            false,
            null,
            null,
            true)
    ],
    new ExternalAuthenticationAdapterCapabilities(true, true, false),
    null);
```

The exact record constructor is release-specific; keep descriptor creation centralized in `Describe()` so server validation and Studio rendering see the same contract.

## Add a custom Studio connection editor

Use a custom editor only when the generic descriptor-driven editor cannot express the provider's configuration UX. The server advertises a `CustomEditorContract`:

```csharp
new CustomEditorContract("contoso-connection", 1)
```

The Studio package registers the matching component and exact contract version:

```csharp
services.AddExternalAuthenticationCustomEditor<ContosoConnectionEditor>(
    "contoso-connection",
    1);
```

`ContosoConnectionEditor` must be a Blazor component implementing `IConnectionCustomEditor`. If it needs to notify the host about unsaved changes, also implement `IConnectionCustomEditorWithChangeTracking` and expose its `Changed` parameter.

Studio resolves an editor only when both the key and contract version match. A missing match uses the generic editor. Duplicate client registrations, an empty key, a non-positive version, or a component that does not implement the marker interface fails registration.

When a custom editor is selected, Studio passes these parameters:

* `Connection`, `Adapter`, and `Model`;
* `ReadOnly`, `CanConfigureUnsafeSettings`, and `CanCreateOverride`;
* managed secret resolver data and its error message;
* `Saved`, `ManagedSecretChanged`, `SecretBindingRemoved`, and `FullOverrideRequested` callbacks; and
* `Changed` when the component implements change tracking.

The custom editor replaces the generic General and Provider editors, so it must render the fields it owns and invoke `Saved` with a `ConnectionMutation`. The Diagnostics tab remains available. Preserve the generic editor's read-only and secret-handling behavior rather than exposing deployment-owned settings or secret values directly in the browser.

## Verification checklist

Before shipping an extension:

1. Register the implementation and its `ExternalAuthenticationExtensionKind`.
2. Set the allowlist explicitly in each deployment environment.
3. Start the host and confirm duplicate, missing, or invalid registrations fail during options validation.
4. Call the descriptor endpoint and verify field types, visibility conditions, unsafe flags, secret redaction, and settings versions.
5. Validate normal, rejected, expired, and replayed authentication callbacks.
6. Test identity-link and permission behavior with a tenant other than the default tenant.
7. If Studio has a custom editor, verify exact key/version matching, read-only mode, save callbacks, secret replacement, and the generic-editor fallback.
8. Add adapter settings migrations before changing a persisted settings shape.

Related operational guidance is in [External Authentication](/guides/authentication/external-authentication), [Configuration](/guides/authentication/external-authentication/configuration), [Administration](/guides/authentication/external-authentication/administration), and [Production and Security](/guides/authentication/external-authentication/production).


# Production and Security

Operate Elsa External Authentication safely in production, including durable state, clustered deployments, migrations, key management, and verification.

> **Preview feature:** Elsa External Authentication is a 3.8 preview capability distributed through Feedz and still under development. Treat it as a controlled rollout: pin a tested preview version, rehearse upgrade and rollback, and retest sign-in, refresh, logout, and recovery paths after every update.

The default broker registration uses in-memory state. That makes setup convenient, but it is a development-only choice: a restart loses active transactions and sessions, and nodes do not share broker state. Production requires durable External Authentication persistence, shared cryptographic material, and an intentional recovery path.

## Production baseline

Before enabling external login for users, confirm all of the following:

* The selected `Elsa.ExternalAuthentication.Persistence.EFCore.<Provider>` package is installed and its External Authentication persistence feature is enabled.
* The `ExternalAuthenticationElsaDbContext` migrations have been applied by your controlled database deployment process.
* Every node uses the same database (or a shared database topology), the same External Authentication handle-hashing key, and a shared ASP.NET Core Data Protection key ring.
* `Redirects:ExternalCallbackBaseUri` is the externally reachable HTTPS Elsa API URL, including the API prefix when applicable.
* The upstream provider has the exact Elsa-derived callback URLs registered.
* Authentication Client callbacks, logout callbacks, origins, and return-path prefixes are exact registrations rather than broad wildcards.
* Elsa Identity signing material and every client/provider secret come from secure deployment configuration, never committed JSON.
* A normal local login, a separate break-glass mechanism, or a deliberate privileged recovery procedure remains available before changing login methods.

## Durable state and EF Core persistence

Enable one provider-specific persistence feature in addition to Identity persistence. The SQLite example uses the same physical database as Identity, but the contexts retain separate migration histories:

```json
{
  "CShells": {
    "Shells": {
      "Default": {
        "Features": {
          "SqliteIdentityPersistence": {
            "ConnectionString": "Data Source=/var/lib/elsa/elsa.db;Cache=Shared"
          },
          "SqliteExternalAuthenticationPersistence": {
            "ConnectionString": "Data Source=/var/lib/elsa/elsa.db;Cache=Shared"
          }
        }
      }
    }
  }
}
```

Use the matching feature for SQL Server, PostgreSQL, MySQL, or Oracle. The External Authentication schema is held in its own `ExternalAuthenticationElsaDbContext`; it can use the same database as Identity or a different one.

The durable feature replaces the in-memory implementations for:

* Database-owned connections and their revisions.
* External identity links.
* Broker transactions and opaque authorization grants.
* External Authentication sessions and rotating refresh-token state.
* Preview results, connection-test observations, and connection-registry versions.

> **Important:** Identity persistence does not imply External Authentication persistence. In 3.8 preview, omitting the dedicated feature silently leaves the broker on in-memory stores. A single node may appear to work until it restarts; multiple nodes have inconsistent sessions, transactions, grants, and registry versions.

Apply the provider's `Initial` migration before enabling traffic. The preview persistence packages use their own migration history. If you are upgrading within 3.8 preview, review the External Authentication persistence migration notes and test against a copy of your database; do not assume the Identity context migration history covers the broker tables.

## Cluster requirements

Every node in a cluster must share these values:

### Handle-hashing key

Set a base64-encoded, random value containing at least 32 bytes of entropy:

```json
{
  "ExternalAuthentication": {
    "HandleHashing": {
      "SharedKeyBase64": "<base64-encoded-32-or-more-byte-random-value>"
    }
  }
}
```

Provide it through a secret provider, not a repository. The key produces non-reversible hashes for opaque handles, external subjects, and secret-generation fingerprints. All nodes using shared persistence must use the same value.

Rotating this key invalidates outstanding broker transactions and changes persisted external-subject hashes. Plan it like an identity migration: stop or drain sign-in traffic, understand the effect on identity links, deploy atomically, and communicate the expected reauthentication impact.

### ASP.NET Core Data Protection

Share the ASP.NET Core Data Protection key ring across every node and persist it outside ephemeral containers. External Authentication protects adapter state and optional upstream logout hints with Data Protection. A node that cannot unprotect state created by another node will fail callbacks or logout continuation.

Use the key storage and protection method appropriate to your platform (for example a protected shared volume, database-backed provider, or managed key store). Keep the application name consistent across nodes that participate in the same broker deployment.

### Public routing

Every Elsa Server broker node must serve the same public base address and API route prefix. Configure load balancing so an upstream callback may reach any healthy broker node; correct shared persistence and Data Protection remove the need for broker session affinity. Ensure reverse proxies forward the public scheme and host correctly, and test the actual externally visible callback URL. Studio Server scale-out has a separate requirement described below.

### Blazor Server Studio scale-out

Broker persistence applies to Elsa Server, not to the Studio host's own login cookie. In this preview, the Blazor Server package stores authentication tickets in a node-local `IMemoryCache` ticket store. A scaled-out Studio Server deployment must therefore use session affinity, or replace the configured ASP.NET Core `ITicketStore` with a shared implementation. Share the Studio host's Data Protection key ring in either case. WebAssembly Studio does not use this server-side ticket store.

## Secret management and rotation

External Authentication distinguishes two binding ownership models:

* `ownership: external` with `resolverType: configuration` points to a key in deployment configuration. The value is managed by the platform, never by Elsa Studio.
* `ownership: managed` with `resolverType: elsa-secrets` points to an active Elsa Secret by name. It requires `Elsa.ExternalAuthentication.Secrets` and Elsa Secrets.

In both models, management responses reveal only whether a binding is configured and resolvable. They never return the value or its generation fingerprint.

Use separate secrets for the upstream provider client and each confidential Authentication Client. Do not reuse the Studio-to-broker client secret as the Elsa-to-provider secret.

Rotating an upstream secret changes a connection's effective material revision. Any sign-in transaction started with the old secret generation is invalidated rather than completing against changed credentials. Rotate during a maintenance window or warn users to restart sign-in if a preview/authorization flow is interrupted.

## Network, trust, and endpoint hardening

The broker defaults to HTTPS-only provider endpoints, blocks private-network destinations, validates each redirect, follows no more than three redirects, and limits provider connection/request times to ten seconds. Keep these defaults unless you have an explicit, reviewed reason to change them.

For a development-only local provider, an operator may allow `localhost` and private destinations explicitly. Do not carry that exception into production. In production:

* Keep `ProviderEgress:RequireHttps` enabled.
* Keep `AllowPrivateNetworkDestinations` disabled.
* Use `AllowedHosts` when the deployment knows the permitted provider host names.
* Do not put credentials in proxy URLs; configure any approved outbound proxy separately.
* Prefer OIDC discovery. Manual issuer, endpoint, and signing-key trust requires explicit unsafe-provider-trust authorization and confirmation.

The adapter validates state, nonce, issuer, signature, audience/authorized party, token lifetime, and upstream S256 PKCE. Still protect the surrounding host: enforce TLS, place Elsa behind an appropriately configured reverse proxy, protect administrative access, and monitor failed callback/token attempts.

## Authorization and administrative safety

Grant administrative permissions narrowly. The important External Authentication permission families are connection read/create/update/archive/test/preview, policy management, role assignment, provider unsafe-trust management, permission delegation, identity-link management, and session read/revoke.

Connection edits are security-sensitive. Database-owned updates require `If-Match` with the current ETag to avoid overwriting concurrent changes. Configuration-owned connections are intentionally read-only. Allow configuration overrides only when the deployment has a reviewed need; configuration wins by default when a database row and configuration row have the same effective key and scope.

The final-login-path guard is enabled by default. It prevents an administrator from removing the last normal sign-in path unless another recovery method or an explicitly authorized override is present. Keep it enabled, retain an audited break-glass procedure, and test that procedure before maintenance windows.

## Runbook: validate before enabling a connection

Use this sequence for each new provider connection:

1. Create the connection as a disabled draft. Use discovery URL, exact scopes, secret binding, claim projection, and the least-privilege unlinked policy.
2. Validate the configuration and run a connection test. The broker retains only the latest redacted observation; it becomes stale after material changes.
3. Register the displayed normal callback at the provider. Register the preview callback only if administrators need preview.
4. Use preview to verify provider authentication, claims, identity resolution, and permission outcome. Preview is short-lived, administrator-bound, one-time, and does not create a user, identity link, normal session, or Elsa credential.
5. Enable the connection, use Studio's login chooser, and complete the normal flow.
6. Verify that the browser/client receives only an Elsa completion code then Elsa tokens; provider access/refresh tokens must not appear in URLs, API responses, or Studio.
7. Verify a refresh token rotates successfully, replay of the superseded refresh token revokes the session, connection disable stops new or pending broker flows, and session revocation works.
8. Test local and optional upstream logout, then validate the configured recovery path.

## Monitoring and troubleshooting

The management and security endpoints provide connection observations, previews, identity links, and session administration. Keep access to those endpoints restricted. The optional `AddExternalAuthenticationHealthCheck()` bridge registers a degraded health check tagged `external-authentication` and `optional`; it does not become a readiness dependency unless your host deliberately includes that tag in readiness.

Useful symptoms and first checks:

| Symptom                                   | First checks                                                                                                                                     |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Callback fails after load balancing       | Shared Data Protection keys, durable broker state, same `SharedKeyBase64`, and public callback URL on every node.                                |
| Sign-in works only until restart          | Dedicated External Authentication persistence feature and migrations are missing.                                                                |
| Connection does not appear in Studio      | The connection is disabled, invalid, archived, shadowed, or outside the tenant context; alternatively, the Authentication Client is unavailable. |
| Provider rejects redirect URI             | Register Elsa's derived callback exactly; do not use Studio's direct-OIDC callback for the upstream provider.                                    |
| `flow_changed` after a configuration edit | A connection material revision or secret generation changed during a pending login; restart sign-in.                                             |
| User is authenticated but lacks access    | Review the identity link/unlinked policy, configured grant sources, and final permission allow/deny boundaries.                                  |

Keep audit logs for connection changes, provider-trust overrides, secret binding changes, session revocations, and successful/failed broker outcomes. The broker uses safe public error categories; do not expose upstream provider response bodies or user-existence details in support-facing logs.


# REST API

Endpoint groups, concurrency rules, permissions, and safe automation patterns for the Elsa External Authentication REST API.

{% hint style="warning" %}
These APIs first appear in the Elsa 3.8 preview and remain under active development. Generate clients against the exact preview build you deploy and keep Core and Studio package versions aligned.
{% endhint %}

All paths below are relative to Elsa's configured API prefix, commonly `/elsa/api`. JSON uses camel case. IDs and flow handles are opaque.

Use the Studio management experience for interactive administration. Use these APIs for controlled automation, deployment validation, and custom administrative clients.

## Broker Endpoints

| Purpose                | Method and path                                           | Authentication                       |
| ---------------------- | --------------------------------------------------------- | ------------------------------------ |
| Discover login methods | `GET /external-authentication/login-methods?clientId=...` | Anonymous, registered client context |
| Begin external sign-in | `GET /external-authentication/authorize/{connectionKey}`  | Anonymous, exact callback + PKCE     |
| Begin local sign-in    | `POST /external-authentication/local/authorize`           | Anonymous, exact callback + PKCE     |
| Provider callback      | `GET /external-authentication/callback/{connectionKey}`   | Provider redirect                    |
| Exchange or refresh    | `POST /external-authentication/token`                     | Client and grant dependent           |
| Begin logout           | `POST /external-authentication/logout`                    | Elsa access token                    |

The login-method response deliberately omits provider authority, adapter configuration, upstream client identifiers, tenant data, health, and secrets. A preferred method affects display only; clients must not auto-redirect.

The authorization-code exchange requires S256 PKCE. Confidential clients also authenticate with a deployment-resolved secret. Public clients must not have a client secret. Authorization and refresh codes are single-use; refresh-token replay revokes the External Authentication Session.

## Descriptor Endpoints

These endpoints let Studio render installed adapters and policies without hard-coding provider-specific forms:

```http
GET /external-authentication/descriptors/adapters
GET /external-authentication/descriptors/policies
GET /external-authentication/descriptors/user-matchers
GET /external-authentication/descriptors/permission-sources
GET /external-authentication/descriptors/managed-secret-resolvers
GET /external-authentication/descriptors/permissions
```

They require `external-authentication:connections:read`. A missing or incompatible custom Studio editor falls back to the descriptor-driven generic editor. Studio obtains Elsa role options from the Identity role API; reading that list requires `read:role`, while assigning default create-user roles is governed by `external-authentication:roles:assign` and delegation boundaries.

## Connection Management

| Purpose                                | Method and path                                                                               |
| -------------------------------------- | --------------------------------------------------------------------------------------------- |
| List and filter                        | `GET /external-authentication/connections`                                                    |
| Create disabled draft or full override | `POST /external-authentication/connections`                                                   |
| Read detail                            | `GET /external-authentication/connections/{connectionId}`                                     |
| Replace document                       | `PUT /external-authentication/connections/{connectionId}`                                     |
| Validate locally                       | `POST /external-authentication/connections/{connectionId}/validate`                           |
| Test provider                          | `POST /external-authentication/connections/{connectionId}/test`                               |
| Begin preview                          | `POST /external-authentication/connections/{connectionId}/preview`                            |
| Enable or disable                      | `POST /external-authentication/connections/{connectionId}/enable` or `/disable`               |
| Archive or restore                     | `DELETE /external-authentication/connections/{connectionId}` or `POST .../restore`            |
| Replace managed secret                 | `PUT /external-authentication/connections/{connectionId}/secret-bindings/{fieldName}/managed` |
| Remove managed secret                  | `DELETE /external-authentication/connections/{connectionId}/secret-bindings/{fieldName}`      |

### Optimistic Concurrency

Connection detail responses include an `ETag`, for example:

```http
ETag: "17"
```

Every database-owned mutation must send that value in `If-Match`. A stale revision returns `412 precondition_failed`; reload the resource, review the current document, and deliberately retry. Configuration-owned resources reject mutation because configuration remains deployment-owned.

### Safe Validation, Test, and Preview

* **Validate** checks structure and binding state without provider traffic.
* **Test** contacts the provider using the exact connection revision and records a safe observation.
* **Preview** performs an administrator-bound, one-time sign-in and returns only an allowlisted result. It does not create a user, identity link, normal completion code, credential, or normal session.

The provider must register the `callbackUri` and, when preview is used, the distinct `previewCallbackUri` returned by the connection resource.

### Lifecycle Safeguards

Disabling or archiving the final normal login method returns a conflict unless another local, external, or deployment-owned recovery path remains. A privileged override requires explicit confirmation. Restored connections remain disabled until they are reviewed and enabled.

## External Identity Links

```http
GET    /external-authentication/user-options?search=&cursor=&pageSize=25
GET    /external-authentication/identity-links?userId=&connectionKey=&cursor=&pageSize=100
POST   /external-authentication/identity-links
POST   /external-authentication/identity-links/{linkId}/replace
DELETE /external-authentication/identity-links/{linkId}
```

These operations require `external-authentication:links:manage`. The issuer must be an absolute HTTPS URI. Serve the management API only over TLS because the request contains the upstream subject. Elsa normalizes and immediately transforms the subject to a keyed hash; the raw subject is never returned.

Create and replace requests identify the Elsa User, immutable connection key, validated issuer, and provider subject. Replacing a link is atomic and gives the replacement a new ID.

## External Authentication Sessions

When session administration is enabled:

```http
GET    /external-authentication/sessions?userId=&connectionKey=&status=&cursor=&pageSize=100
DELETE /external-authentication/sessions/{sessionId}
```

Read requires `external-authentication:sessions:read`; revoke requires `external-authentication:sessions:revoke`. Responses contain only safe metadata—never tokens, token hashes, external subjects, or claim snapshots.

## Permissions

| Area                                   | Permissions                                                                        |
| -------------------------------------- | ---------------------------------------------------------------------------------- |
| Read/create/update/archive connections | `external-authentication:connections:read`, `:create`, `:update`, `:archive`       |
| Test and preview                       | `external-authentication:connections:test`, `:preview`                             |
| Unsafe provider trust                  | `external-authentication:provider-trust:unsafe`                                    |
| Policies and roles                     | `external-authentication:policies:manage`, `external-authentication:roles:assign`  |
| Identity links                         | `external-authentication:links:manage`                                             |
| Sessions                               | `external-authentication:sessions:read`, `external-authentication:sessions:revoke` |

The Elsa API is the authorization boundary. Hidden menus and disabled buttons in Studio are usability affordances, not security controls.

## Error Handling

Public errors use safe categories such as `invalid_request`, `method_unavailable`, `authentication_failed`, `identity_unlinked`, `flow_expired`, `flow_changed`, `access_denied`, `rate_limited`, and `temporarily_unavailable`. Management APIs add categories such as `validation_failed`, `conflict`, and `precondition_failed`.

Provider response bodies, secret material, tokens, raw subjects, and tenant/user existence details are never included. Log the returned `correlationId` and use server-side diagnostics to investigate.

## Automation Checklist

* Use TLS for every browser, provider, and management endpoint.
* Treat all IDs, codes, state, ETags, and handles as opaque.
* Honor `Retry-After` on `429` responses.
* Never log authorization codes, refresh tokens, client secrets, or raw subjects.
* Send `If-Match` for every database-owned connection mutation.
* Do not copy configuration-owned connections or external secret values through the API.
* Re-read state after conflicts instead of retrying blindly.
* Give automation clients only the permissions required for their task.

## Related Guides

* [Configuration Reference](/guides/authentication/external-authentication/configuration)
* [Administration in Studio](/guides/authentication/external-authentication/administration)
* [Production and Security](/guides/authentication/external-authentication/production)
* [Troubleshooting](/guides/authentication/external-authentication/troubleshooting)


# Migrate from Direct OIDC

Move Elsa Studio from direct OpenID Connect to the Elsa 3.8 preview External Authentication broker, with a staged rollout and rollback plan.

> **Preview feature:** Brokered External Authentication is new in Elsa 3.8 preview and is currently available from Feedz while under active development. Plan this as a reversible, staged migration; do not remove the existing direct OpenID Connect registration until the brokered flow is proven in your environment.

Direct OpenID Connect remains a supported Studio authentication mode. External Authentication is an alternative architecture: Elsa Server becomes the relying party for the upstream provider, then issues Elsa credentials that Studio consumes.

This gives Elsa a central connection-management, identity-link, session-administration, and permission-mapping surface. It also changes which application owns the provider client registration and callback URL.

## Choose exactly one Studio authentication mode

Each Studio host must set `Authentication:Provider` to one mode:

* `OpenIdConnect` for the existing direct provider integration.
* `ExternalAuthentication` for brokered login through Elsa Server.
* `ElsaIdentity` for direct Elsa local-credential integration.

Do not register direct OpenID Connect and brokered External Authentication in the same Studio host. The configuration is intentionally rejected when startup would be ambiguous.

## What changes

| Direct Studio OIDC setting                                   | Brokered External Authentication destination                                                             |
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
| `Authentication:OpenIdConnect:Authority` or metadata address | Configuration-owned or database-owned Elsa connection `adapterSettings.discoveryUrl` and discovery mode. |
| Studio `ClientId` for the upstream provider                  | Connection `adapterSettings.clientId`: Elsa Server's upstream provider registration.                     |
| Studio upstream `ClientSecret`                               | Connection `secretBindings.clientSecret`, resolved only by Elsa Server.                                  |
| `AuthenticationScopes`                                       | Connection `adapterSettings.scopes`.                                                                     |
| Provider callback path                                       | Elsa's fixed derived provider callback, based on the connection key.                                     |
| Studio signed-out callback                                   | Elsa upstream logout callback when upstream logout is enabled.                                           |
| Name/role claim configuration                                | Claim projection plus explicit identity-link/unlinked policy and permission-grant mapping.               |
| Backend API scopes                                           | No direct equivalent. Studio calls Elsa with Elsa-issued credentials after broker code exchange.         |

There are now **two** client registrations:

1. The **upstream provider client**, owned by the Elsa connection. It is normally confidential and belongs to Elsa Server.
2. The **Elsa Authentication Client**, owned by the deployment. It identifies Studio to the broker, owns exact Studio callback/logout/origin/return-path registrations, and grants no Elsa permissions by itself.

Never copy either client secret through Studio UI or APIs. Configure the same Studio confidential-client secret separately in Elsa Server and the Studio Server host through their respective secret stores.

## Staged migration

1. Keep Studio in `Authentication:Provider: OpenIdConnect`. Do not change or delete its current configuration.
2. Install and configure `Elsa.ExternalAuthentication` plus `Elsa.ExternalAuthentication.OpenIdConnect` on Elsa Server. For production, add one dedicated `Elsa.ExternalAuthentication.Persistence.EFCore.<Provider>` package and feature.
3. Configure Elsa Identity token signing, the deployment callback base URI, a configuration-owned OIDC connection, and an Authentication Client for the Studio host. See [Installation](/guides/authentication/external-authentication/installation) and [Configuration](/guides/authentication/external-authentication/configuration).
4. Register Elsa's derived normal callback with the upstream provider. Keep Studio's existing direct callback registered during the migration.
5. Resolve secrets independently in Elsa Server and Studio Server. The migration must never move or reveal a secret.
6. Validate and test the connection, then run an administrator preview and a normal brokered login in a non-production environment.
7. Change only `Authentication:Provider` to `ExternalAuthentication` in the Studio host, configure the broker client options, and restart the host.
8. Verify sign-in, token refresh, logout, session revocation, user/role mapping, and the break-glass/recovery path.
9. Only after a successful observation period, retire the direct provider callback and rotate/delete direct-only secrets according to your own change-control process.

## Studio Server configuration

Studio Server is a confidential Authentication Client. It exchanges the broker completion code on the server, keeps Elsa access and refresh credentials on the server, and gives the browser a secure HTTP-only Studio session cookie.

```json
{
  "Authentication": {
    "Provider": "ExternalAuthentication",
    "ExternalAuthentication": {
      "ClientId": "elsa-studio-server",
      "ClientSecret": "<read-from-server-secret-provider>",
      "CallbackPath": "/authentication/external/callback",
      "LogoutCallbackPath": "/authentication/external/logout-callback"
    }
  }
}
```

Register the Studio host integration:

```csharp
builder.Services.AddExternalAuthenticationBroker(options =>
    builder.Configuration.GetSection("Authentication:ExternalAuthentication").Bind(options));

builder.Services.AddExternalAuthenticationModule(backendApiConfig);
```

The `ClientId`, callback path, and logout callback path must exactly match the Authentication Client registered in Elsa Server. Supply `ClientSecret` through deployment configuration, for example `Authentication__ExternalAuthentication__ClientSecret`; never place it in a browser-delivered configuration file.

## Studio WebAssembly configuration

Studio WebAssembly is a public Authentication Client. It has no secret and always uses S256 PKCE. Register its exact browser origin in the Elsa Authentication Client.

```json
{
  "Authentication": {
    "Provider": "ExternalAuthentication",
    "ExternalAuthentication": {
      "ClientId": "elsa-studio-wasm",
      "CallbackPath": "/authentication/external/callback",
      "LogoutCallbackPath": "/authentication/external/logout-callback",
      "BrowserStorage": "Memory"
    }
  }
}
```

`Memory` is the secure default and requires a new sign-in after browser reload. `Session` and `Durable` storage are explicit deployment choices that retain credentials in browser-accessible storage and should produce a visible security warning. Never add a client secret to a WASM configuration file.

## Callback registration

Studio's callback is where Elsa sends an opaque completion code after successful broker authentication. The **upstream provider** must instead redirect to Elsa Server:

```
https://elsa.example.com/elsa/api/external-authentication/callback/<connection-key>
```

If previews are enabled for administrators, register the additional preview callback:

```
https://elsa.example.com/elsa/api/external-authentication/previews/callback/<connection-id>
```

Do not substitute `/signin-oidc`, `/authentication/login-callback`, or the Studio broker callback as the provider callback. Elsa validates the provider response and redirects Studio only with an opaque code and client state; provider and Elsa tokens do not appear in the URL.

## Compatibility and rollback

The direct `Authentication:OpenIdConnect` section is not rewritten or consumed by brokered configuration. Keeping it intact is the compatibility guarantee that makes rollback simple.

To roll back a Studio Server or WASM host:

1. Restore `Authentication:Provider` to `OpenIdConnect`.
2. Restart the Studio host.
3. Confirm Studio is using its retained direct client registration and callback.

The Elsa broker configuration, provider callback, and secrets can remain in place while you investigate. Do not remove them or revoke the direct client until you decide to complete the migration. A broker rollback does not require moving secrets back because the staged migration kept them separate.

## Verification checklist

* The login-method endpoint lists the expected connection without disclosing provider URL, adapter settings, client ID, remote icon URL, health details, or secrets.
* Selecting the connection redirects first to the provider and returns only to Elsa's connection-key callback.
* Elsa redirects Studio with a one-time completion code; replay fails.
* A confidential Studio Server exchanges the code server-side; a WASM client exchanges it with PKCE and no secret.
* The Elsa access token contains permissions produced by Elsa roles or explicit configured grant sources—not unbounded upstream claims.
* Disabling a connection blocks new initiation, pending callback, and external refresh. Existing Elsa access tokens follow their configured expiry.
* Local logout works. If upstream logout is enabled, confirm the provider callback returns only to the exact registered Studio logout callback.
* Restoring `OpenIdConnect` and restarting Studio successfully returns to direct authentication before you retire the old registration.


# Troubleshooting

Diagnose Elsa Studio External Authentication integration and administration issues in Elsa 3.8 preview.

> **Preview feature — Elsa 3.8.** External Authentication currently ships from the Feedz.io preview feed and remains under development. Check that Elsa Core and Elsa Studio use compatible 3.8 preview builds before investigating a runtime symptom as a configuration problem.

Start with the boundary that failed: Studio host startup, login method discovery, broker redirect/callback, token exchange/refresh, Elsa API authorization, or management API authorization. Do not capture or share client secrets, access tokens, refresh tokens, authorization codes, PKCE verifiers, or full callback URLs in logs or support tickets.

## Fast triage

1. Record the Studio host model (Blazor Server or WebAssembly), public Studio origin, Elsa API `Backend:Url`, and the preview package versions—without secret values.
2. Confirm `Authentication:Provider` is exactly `ExternalAuthentication`.
3. Confirm Studio's `ClientId` identifies a dedicated Elsa Authentication Client, not an Elsa API Application.
4. Compare the absolute callback/logout URLs registered in Elsa Server with the public Studio URLs character-for-character.
5. Check the server-side External Authentication feature and the affected user's Elsa `permissions` claims.
6. Use a redacted **Test connection** or **Preview sign-in** observation where the failure is provider/policy related.

## Startup failures

### “External Authentication requires a configured broker client ID”

`Authentication:ExternalAuthentication:ClientId` is missing, empty, or bound from the wrong configuration source.

Set a non-empty client ID for the dedicated Studio Authentication Client:

```json
{
  "Authentication": {
    "Provider": "ExternalAuthentication",
    "ExternalAuthentication": {
      "ClientId": "elsa-studio-server"
    }
  }
}
```

Use the corresponding public client ID in WASM. Do not reuse an Elsa API application client just because it has an ID.

### “Studio Server must configure a confidential broker client secret”

The Blazor Server package requires `ClientSecret`. Put the value in deployment secret configuration and ensure the process can read it. Do not add it to the repository or a browser-delivered appsettings file.

### “Studio WebAssembly is a public client and must not contain a broker client secret”

Remove `ClientSecret` from all WASM configuration inputs. Any value published to WASM is public, including a value injected at build time. Create/use a public Elsa Authentication Client for that host instead.

### Fixed callback path validation fails

These settings cannot be customized:

```json
{
  "CallbackPath": "/authentication/external/callback",
  "LogoutCallbackPath": "/authentication/external/logout-callback"
}
```

Restore the fixed paths in configuration and change the client registration at Elsa Server/upstream provider to match the absolute Studio URLs. Do not try to solve a path-base or proxy issue by changing these values.

### Startup rejects authentication registrations

The host has mixed legacy login, Elsa Identity, direct OIDC, and/or brokered registration. Select one `Authentication:Provider` and register only the corresponding host authentication package. The shared External Authentication management module is separate from selecting the broker login mode.

## Login methods missing or sign-in does not start

### `/login` shows no expected external provider

Check these in order:

1. The Studio host selects `ExternalAuthentication` and successfully binds its client ID.
2. `Backend:Url` reaches the Elsa API used by the configured Authentication Client.
3. The Elsa Server connection is enabled, valid, unarchived, unshadowed, and effective in the request's tenant context.
4. The Server External Authentication feature is enabled/advertised.
5. The login-method request for the Studio client completes successfully.

The preferred method is only a visual hint. It never navigates automatically; the user must select it. If a preferred method fails, the chooser should still allow another available method.

### WASM login discovery fails with a CORS error

Register the exact Studio origin twice: in the public Elsa Authentication Client's `AllowedOrigins` collection and in the Elsa Server host's ASP.NET Core CORS policy. These controls serve different purposes; `AllowedOrigins` does not make Elsa API responses available cross-origin.

Apply the CORS policy before authentication, authorization, and endpoint mapping, and allow the headers and methods required by Studio and SignalR. Do not use `AllowAnyOrigin` with credentialed requests. Confirm scheme, host, and port exactly—changing only the Studio client registration cannot fix an API CORS rejection.

### Test succeeds but sign-in returns `method_unavailable`

A successful metadata test proves that the adapter can resolve provider metadata; it does not prove that the connection is currently eligible for the requested broker flow. Check the effective connection selected for the current tenant and client:

* it is enabled, valid, and not archived;
* a database override is not shadowing the configuration-owned record;
* the Authentication Client itself is enabled and the request uses its exact registered callback and expected tenant context;
* the adapter module is installed and its flow is available in this preview build.

Use the public error category and correlation ID to locate the matching server-side security event. Do not expose provider exceptions or retry captured callback URLs.

### Local broker credentials fail

Use the broker-local form only if Elsa Server advertises a local method. For Blazor Server, local credentials post to `/authentication/external/local-login` with antiforgery protection; they are not placed in the browser address bar. Verify the browser is returning the antiforgery cookie/form token and that any proxy does not strip the POST body.

For WASM, do not inspect browser history or logs for passwords. Confirm the broker local-authorize endpoint is reachable and use a known test account.

### Callback returns to the chooser

Studio intentionally returns to `/login?choose=true` on an invalid callback, provider error, missing code, expired state, or replayed state. Check for:

* The callback is exactly the registered scheme, host, port, and path.
* The callback belongs to the same Studio origin for WASM.
* The user completed the flow in the same tab that initiated it (especially with `BrowserStorage: Memory` or `Session`).
* The callback transaction did not exceed its short lifetime or get consumed by a previous attempt.
* No load balancer/proxy changed the Server host's public scheme or host.

Do not retry a captured callback URL: completion codes and state are one-time values by design.

### Return path is unexpectedly `/`

Studio accepts only a local relative return path beginning with one `/`. It normalizes an empty path, absolute URL, `//...`, backslash-containing path, or invalid relative URI to `/` to prevent open redirects. Use a local Studio path such as `/workflows?tab=active` and add the necessary allowed local return path to the Authentication Client.

## Token, refresh, API, and SignalR failures

### Sign-in works but Studio API calls return `401`

Check that:

* `Backend:Url` is the Elsa API base URL expected by the host.
* The code exchange completed and the host could create an Elsa access token.
* The Studio Authentication Client and callback URI match the selected host.
* Server-side application instances share any required ticket/cache state when running more than one Server host node.
* The browser storage choice matches the symptom: `Memory` deliberately loses credentials after reload/new tab/tab close.

For Server hosts, cookies are secure and HTTP-only. Test over HTTPS; an HTTP origin will not receive the secure session cookie. For WASM, verify the browser loaded `external-authentication.js` before Blazor starts and that it is served from the same deployed package version.

### API calls return `403`

This usually means authentication succeeded but authorization did not. Inspect the current Elsa access token or server-side authorization diagnostics using safe internal tooling and confirm `permissions` claims contain the required Elsa endpoint permission. A Studio menu item being visible is not proof that the API will authorize the action.

For External Authentication administration, verify the exact permission, such as `external-authentication:connections:read`, rather than a similarly named role. See [External Authentication administration](/guides/authentication/external-authentication/administration#routes-and-permissions).

### WASM user is signed out after refresh/reload

This is expected with `BrowserStorage: Memory`, the default. Select `Session` only when retaining the session within a tab is worth the additional browser token exposure; select `Durable` only when persistent local storage is an explicit, reviewed requirement. Both alternatives emit a security warning and increase exposure to script compromise.

### SignalR disconnects or fails after signing in

Confirm the host selected the External Authentication API handler and broker registration, because the package also registers the SignalR HTTP connection options configurator. Then check normal cross-origin/proxy/WebSocket settings, the current access-token lifetime, and whether a WASM memory session was lost.

## Logout problems

### Local logout works but upstream logout does not

Local sign-out is intentionally independent of upstream availability. For upstream logout, verify that the selected connection supports it and that the registered `LogoutCallbackPath` is exactly `/authentication/external/logout-callback` at the public Studio origin.

Studio accepts an upstream continuation only when Elsa Server returns a same-origin route containing `/external-authentication/logout/continue/`. Treat a different or external continuation URL as a configuration/security problem, not a URL to follow manually.

### Server logout request returns antiforgery errors

The Server logout is an authenticated POST protected by antiforgery. Ensure the page rendered the form token, the secure cookie is present on HTTPS, and any reverse proxy preserves cookies and POST form values.

## Management UI and connection issues

### Identity & access menu is missing

Confirm that the shared `AddExternalAuthenticationModule(backendApiConfig)` registration is present, Elsa Server advertises the External Authentication feature, and the current user has the route's menu permission. For example, connection management requires `external-authentication:connections:read`; identity links require `external-authentication:links:manage`.

### Connection cannot be edited

A configuration-owned connection is intentionally read-only. Change it in deployment configuration, or create a complete database override only if Elsa Server advertises `canCreateOverride` and the operator has create permission. The override does not copy secret bindings; configure its required secrets again.

### A secret field cannot be edited or removed

Studio exposes a write-only managed editor only if Elsa Server advertises an installed managed-secret resolver. It does not show deployment-managed secret references. A required managed secret cannot be removed while the connection is enabled—disable it first.

If a secret was pasted into a log, configuration commit, or support artifact, rotate it in the provider and managed secret store; deleting the text does not make the exposure safe.

### Test connection or preview fails

Use the redacted test result's category, summary, warnings, and correlation ID to investigate server/provider logs. Ensure the current connection revision is saved first—test and preview operate on the current revision.

Preview opens in a separate tab and returns a one-time redacted result; it does not create a user, link, credential, or session. A preview failure does not alter normal accounts. Do not expect the page to reveal provider access tokens or raw claims.

### Cannot disable a connection

When disabling would remove the final normal login method, Studio requires an explicit recovery override confirmation. Verify an independent recovery path first, including break-glass access where applicable. You may optionally revoke active sessions if you have `external-authentication:sessions:revoke`; otherwise existing sessions can remain active until expiry or separate revocation.

### Archived connection does not reappear as a login method

Restore returns it as **disabled**, by design. Test/validate it and explicitly enable it. Also check whether another database override shadows a configuration-owned connection.

### Identity link action is denied or has unexpected sign-in results

Confirm `external-authentication:links:manage`, tenant context, Elsa user ID, connection key, issuer, and subject. Replacing a link resets its sign-in history; unlinking prevents that external identity from signing in until an appropriate new link or policy permits it.

### Session page is incomplete

The page deliberately contains only safe metadata and never returns tokens, external subject values, or claim snapshots. Confirm `external-authentication:sessions:read` for viewing and `external-authentication:sessions:revoke` to revoke. This is a privacy/security boundary, not a data-loading error.

## Verification commands

For Studio source builds, run the module test project:

```bash
dotnet test src/modules/Elsa.Studio.ExternalAuthentication.Tests/Elsa.Studio.ExternalAuthentication.Tests.csproj
```

The browser suite exercises browser-only PKCE, storage scope, reload/tab behavior, refresh-token rotation/reuse, callback replay, logout, and accessibility:

```bash
cd tests/browser/ExternalAuthentication
npm install
npx playwright install
npm test
```

Use a non-production test tenant/client for browser diagnostics. The suite is designed to prove that tokens and secrets do not leak into URLs and that a preferred method does not start without user input.

For setup and configuration details, see [Studio integration](/guides/authentication/external-authentication/studio-integration). For safe management workflows, see [External Authentication administration](/guides/authentication/external-authentication/administration).


# Elsa API Permissions

Release-backed reference for Elsa API permission claims, Studio capabilities, and least-privilege role templates in Elsa 3.8.0.

Elsa API authorization is claim-based. When API security is enabled, Elsa endpoints look for claims with the type `permissions`. Each claim value is an Elsa permission string, such as `read:workflow-definitions`. The `*` value grants all Elsa API permissions.

This page is a practical map for configuring Elsa Identity roles, external identity providers, API-key applications, and Studio users. It covers the common routes in the `release/3.8.0` API; modules can add more permissions of their own.

For authentication middleware, tokens, and external provider setup, see [Authentication & Authorization](/guides/authentication) and [Direct OpenID Connect](/guides/authentication/direct-openid-connect). For the .NET API client, see [API & Client](/guides/api-client).

## How permissions are applied

Elsa's API endpoint base classes call `ConfigurePermissions(...)`. With security enabled, this registers the endpoint with the supplied permissions and the `*` wildcard. With security disabled, the same helper permits anonymous access. The permission strings are therefore part of the API contract, not ASP.NET Core role names.

Elsa Identity obtains permissions from roles:

* JWT access tokens include the permissions of the user's assigned roles.
* API keys include the permissions of the roles assigned to the application that owns the key.
* An external OIDC or JWT host must emit `permissions` claims itself or map provider roles, groups, or scopes into that claim type during token validation.

An ASP.NET Core policy such as `RequireRole("WorkflowOperator")` can protect a custom controller or page, but it does not satisfy an Elsa endpoint that is checking `permissions`.

### A permission is one claim value

Emit one claim for each permission. For example, a service identity that can read definitions and execute them has claims equivalent to:

```csharp
using System.Security.Claims;

var claims = new[]
{
    new Claim("permissions", "read:workflow-definitions"),
    new Claim("permissions", "exec:workflow-definitions")
};
```

When using Elsa Identity, configure the values on a role instead of creating these claims manually. Avoid using `*` for user-facing or machine identities unless full administrative access is genuinely intended.

## Common API permissions

The following table maps the most frequently used management operations to the permissions declared by the release API.

| Operation                                               | Permission                     | Typical routes                                                                               |
| ------------------------------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------- |
| Read workflow definitions and versions                  | `read:workflow-definitions`    | `/workflow-definitions`, `/workflow-definitions/{id}`, `/workflow-definitions/{id}/versions` |
| Create or update a definition                           | `write:workflow-definitions`   | `POST /workflow-definitions`, import routes                                                  |
| Publish a definition or update references               | `publish:workflow-definitions` | `/workflow-definitions/{id}/publish`, reference updates, version revert                      |
| Retract a definition                                    | `retract:workflow-definitions` | `/workflow-definitions/{id}/retract`                                                         |
| Delete definitions or versions                          | `delete:workflow-definitions`  | delete and bulk-delete routes                                                                |
| Execute or dispatch a definition                        | `exec:workflow-definitions`    | `/workflow-definitions/{id}/execute`, `/dispatch`, bulk dispatch                             |
| Read workflow instances, variables, and journal entries | `read:workflow-instances`      | `/workflow-instances` and its journal, variables, and execution-state routes                 |
| Update root-workflow variables or import an instance    | `write:workflow-instances`     | `/workflow-instances/{id}/variables`, instance import                                        |
| Cancel an instance                                      | `cancel:workflow-instances`    | `/cancel/workflow-instances/{id}` and bulk cancel                                            |
| Delete instances                                        | `delete:workflow-instances`    | delete and bulk-delete routes                                                                |
| Read activity executions and call stacks                | `read:activity-execution`      | `/activity-executions` and summary routes                                                    |
| Read activity and designer metadata                     | See the metadata table below   | `/descriptors/*`, `/features`, `/storage-drivers`, and related routes                        |

The route prefix is host-configurable. These route examples are relative to the Elsa API base, which is commonly `/elsa/api`.

### Designer metadata

Give a Studio designer the metadata permissions required by the modules and expression languages installed by the host. The release API declares these permissions for the corresponding endpoints:

| Capability                     | Permission                            |
| ------------------------------ | ------------------------------------- |
| Activity descriptors           | `read:activity-descriptors`           |
| Activity descriptor options    | `read:activity-descriptors-options`   |
| Expression descriptors         | `read:expression-descriptors`         |
| Storage drivers                | `read:storage-drivers`                |
| Variable descriptors           | `read:variable-descriptors`           |
| Installed feature list         | `read:installed-features`             |
| Workflow activation strategies | `read:workflow-activation-strategies` |
| Commit strategies              | `read:commit-strategies`              |
| Incident strategies            | `read:incident-strategies`            |
| Log persistence strategies     | `read:log-persistence-strategies`     |

The expression-descriptor endpoint additionally filters C# and Python descriptors unless the caller has `exec:csharp-expressions` or `exec:python-expressions`, respectively. Grant those execution permissions only when the role is allowed to author or execute those expression types.

The descriptor and feature endpoints also accept the broader `read:*` value in release 3.8.0. Prefer the named permissions for least privilege.

## Runtime and operational permissions

| Operation                          | Permission                                         | Notes                                       |
| ---------------------------------- | -------------------------------------------------- | ------------------------------------------- |
| Read runtime status                | `read:workflow-runtime` or `ManageWorkflowRuntime` | The status endpoint advertises both values. |
| Pause the runtime                  | `ManageWorkflowRuntime`                            | Administrative state change.                |
| Resume the runtime                 | `ManageWorkflowRuntime`                            | Administrative state change.                |
| Force-drain the runtime            | `ManageWorkflowRuntime`                            | Administrative state change.                |
| Read bookmark queue dead letters   | `read:bookmark-queue:dead-letters`                 | Lists or reads dead-letter items.           |
| Replay bookmark queue dead letters | `replay:bookmark-queue:dead-letters`               | Requeues failed bookmark work.              |
| Delete bookmark queue dead letters | `delete:bookmark-queue:dead-letters`               | Destructive cleanup.                        |
| Read alterations                   | `read:alterations`                                 | Inspect alteration requests.                |
| Run or submit alterations          | `run:alterations`                                  | Applies or evaluates alteration work.       |

`ManageWorkflowRuntime` is intentionally a distinct claim from `read:workflow-runtime`; do not give it to a user who only needs status visibility.

## Module-specific permissions

Install only the modules the host needs, then add their claims to the relevant role. These are the permission values declared by common release modules:

| Module or capability        | Read/view                                               | Write or action                                              |
| --------------------------- | ------------------------------------------------------- | ------------------------------------------------------------ |
| Dashboard                   | `read:dashboard`                                        | —                                                            |
| Console logs                | `read:diagnostics:console-logs`                         | —                                                            |
| Structured logs             | `read:diagnostics:structured-logs`                      | —                                                            |
| OpenTelemetry diagnostics   | `read:diagnostics:opentelemetry`                        | —                                                            |
| Secrets                     | `read:secrets`                                          | `write:secrets`, `delete:secrets`, `test:secrets`            |
| Labels                      | `read:labels`                                           | `create:labels`, `update:labels`, `delete:labels`            |
| Workflow-definition labels  | `read:workflow-definition-labels`                       | `update:workflow-definition-labels`                          |
| Tenants                     | `read:tenants`                                          | `write:tenants`, `delete:tenants`, `execute:tenants:refresh` |
| Identity users              | `read:user`                                             | `create:user`, `update:user`, `delete:user`                  |
| Identity roles              | `read:role`                                             | `create:role`, `update:role`, `delete:role`                  |
| Identity applications       | —                                                       | `create:application`                                         |
| Resilience diagnostics      | `read:resilience:strategies`, `read:resilience:retries` | `exec:resilience:simulate-response`                          |
| Authenticated event trigger | —                                                       | `trigger:event`                                              |

For the secret lifecycle, built-in stores, Studio picker, and runtime reference contract behind these claims, see the [Secrets Management guide](/guides/security/secrets-management).

Some module endpoints also advertise namespace wildcards such as `read:*` or `exec:*`. Check the exact endpoint in the version you deploy before replacing named claims with a wildcard.

The release also defines an `ingest:diagnostics:opentelemetry` constant, but the checked collector and diagnostics API endpoints do not declare it through `ConfigurePermissions(...)`. Do not assume that claim gates ingestion in 3.8.0; the HTTP collector instead checks its configured API key header or an explicitly allowed loopback request. Verify the host and collector path you deploy.

REST and live-connection checks are not identical for every module. The Console Logs, OpenTelemetry, and Workflow Instance hubs explicitly accept their read permission (plus the relevant wildcards). The Structured Logs hub in 3.8.0 has an authentication requirement but no module-specific permission authorizer, even though its REST endpoints require `read:diagnostics:structured-logs`.

## Starter role templates

These are starting points, not built-in Elsa roles. Put the listed strings in the `Permissions` array of an Elsa Identity role, then assign that role to a user or application. Add module-specific claims only when that role uses the module.

| Role                        | Start with                                                                                                                                                                                           |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Studio viewer**           | `read:workflow-definitions`, `read:workflow-instances`, `read:activity-execution`, and the required designer metadata claims                                                                         |
| **Workflow designer**       | Studio viewer claims plus `write:workflow-definitions`, `publish:workflow-definitions`, `retract:workflow-definitions`, and `delete:workflow-definitions`                                            |
| **Workflow operator**       | Studio viewer claims plus `cancel:workflow-instances`, `delete:workflow-instances`, and `read:workflow-runtime`                                                                                      |
| **Runtime administrator**   | `read:workflow-runtime` and `ManageWorkflowRuntime`                                                                                                                                                  |
| **Workflow service runner** | Only the definition permissions needed by the service, commonly `read:workflow-definitions` and `exec:workflow-definitions`; add instance read/write claims only if it inspects or changes instances |
| **Identity administrator**  | The `read/create/update/delete` claims for users and roles, plus `create:application` if it provisions API-key applications                                                                          |

The Studio viewer and designer templates are intentionally separate from the runtime administrator template. A user can design workflows without being able to pause or force-drain the runtime.

### Role-management safeguard

Elsa Identity checks the caller's `permissions` claims when roles are created, updated, or assigned. A caller cannot use its identity-management access to grant permissions that it does not itself possess. Keep this in mind when delegating role administration: the administrator must already have every permission it needs to assign.

## What Studio users see

Studio is a client of the Elsa API; it is not a second authorization system. The server remains the authority for each operation:

* In the workflow editor, release Studio treats a workflow as editable when the returned workflow resource includes a `publish` link. Without that capability, version-management actions are read-only or disabled.
* Diagnostics modules are gated by both the corresponding backend feature and permission. For example, missing `read:diagnostics:console-logs` hides the Console navigation item and makes direct navigation unavailable or unauthorized.
* A successful Studio login therefore does not prove that the token can load definitions, instances, designer metadata, or diagnostics. Those calls can still return `403 Forbidden` when the required Elsa claim is missing.

When a Studio screen is incomplete, identify the failing API request first and add the permission for that operation. Do not grant `*` just to make a single screen load.

## Boundaries that use different authorization

Elsa API permissions do not apply to every URL hosted by an Elsa application:

* Workflow routes handled by the `HttpEndpoint` activity use the activity's `Authorize` and `Policy` settings. See [HTTP Endpoint Security](/guides/security/http-endpoint-security).
* Bookmark-resume endpoints are intentionally anonymous in the release API; the encrypted resume token is the capability. Treat it as a secret and generate it with a bounded lifetime. See [API & Client](/guides/api-client) and [Bookmark Resume Tokens](/guides/security/bookmark-resume-tokens).
* Custom host controllers and pages can use ordinary ASP.NET Core policies, roles, or claims independently of Elsa API permissions.

## Troubleshooting missing access

1. Confirm API security is enabled or disabled as intended. When disabled, Elsa endpoint permission checks are bypassed.
2. Identify the exact API route returning `401` or `403`.
3. Check the final authenticated principal on the Elsa Server and verify that its claims use the literal type `permissions`.
4. Add the exact permission declared by that endpoint, or map the external provider's role/group/scope to it.
5. If the endpoint is a Studio metadata route, check the corresponding designer permission table above. If it is a module route, check the module-specific table and whether the module is installed.

`401` usually means the request was not authenticated. `403` usually means it was authenticated but lacked an accepted permission or failed another policy.

### Release source checked

This reference was checked against the remote `origin/release/3.8.0` commits in `elsa-core` and `elsa-studio`. The local `elsa-core` branch with the same name had diverged, so it was not used as release evidence.


# Custom Authentication

Integrate a host-provided ASP.NET Core authentication scheme with Elsa API permissions.

Elsa Server uses ASP.NET Core authentication. A host can therefore use a custom or organization-standard authentication scheme instead of Elsa Identity, provided the resulting principal is authenticated and carries the permission claims required by the Elsa API.

Use this path when the host already owns authentication or when a protocol is not covered by Elsa Identity, Direct OpenID Connect, or External Authentication.

## Register the host scheme

Configure the scheme through standard ASP.NET Core services, add authorization, and place the middleware before the Elsa API:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddAuthentication("CompanyScheme")
    .AddScheme<AuthenticationSchemeOptions, CompanyAuthenticationHandler>(
        "CompanyScheme",
        _ => { });

builder.Services.AddAuthorization();

builder.Services.AddElsa(elsa =>
{
    elsa
        .UseWorkflowManagement()
        .UseWorkflowRuntime()
        .UseWorkflowsApi();
});

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.UseWorkflowsApi();
app.Run();
```

The handler implementation, credential validation, challenge behavior, and key management belong to the host application.

## Map Elsa permissions

Elsa API endpoints authorize against claims whose type is `permissions`. Translate only trusted upstream roles, groups, or scopes into the named Elsa permissions required by the caller.

Do not treat a successful login, a generic `Admin` role, or an arbitrary external scope as full Elsa access. ASP.NET Core role policies can protect custom host endpoints, but they do not replace Elsa endpoint permissions.

See [Elsa API Permissions](/guides/authentication/permissions).

## Configure Studio separately

Studio must obtain a credential that the custom server scheme accepts and send it with every Elsa API request. Depending on the topology, that may require a custom Studio authentication module or HTTP message handler.

If the provider is OpenID Connect, prefer the supported [Direct OpenID Connect](/guides/authentication/direct-openid-connect) or [External Authentication](/guides/authentication/external-authentication) modules before building a custom integration.

## Keep workflow ingress separate

The `HttpEndpoint` activity uses its own `Authorize` and `Policy` settings. Registering a custom Elsa API scheme does not automatically secure workflow routes. See [HTTP Endpoint Security](/guides/security/http-endpoint-security).

## Validation checklist

* Anonymous API requests receive the expected challenge or denial.
* Valid credentials create an authenticated principal.
* Missing Elsa permissions produce `403 Forbidden` responses.
* The principal receives only the permissions derived from trusted claims.
* Studio refresh, logout, and expired-credential behavior are tested.
* Logs and traces do not contain credentials or token contents.


# Disable Authentication in Development

Disable Elsa API and Studio authorization for isolated development and test environments only.

Disabling authentication is useful for local learning, prototypes, and isolated automated tests. It must never be used for a shared, staging, or production environment.

{% hint style="danger" %}
Disabling security exposes Elsa management APIs to every caller that can reach the host. Keep the host on a trusted local interface and restore authentication before deploying it anywhere else.
{% endhint %}

## Disable Elsa API endpoint security

Call `EndpointSecurityOptions.DisableSecurity()` before mapping the Elsa API. Guard it with the host environment so the setting cannot silently reach production:

```csharp
using Elsa;

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
    EndpointSecurityOptions.DisableSecurity();

builder.Services.AddElsa(elsa =>
{
    elsa
        .UseWorkflowManagement()
        .UseWorkflowRuntime()
        .UseWorkflowsApi();
});

var app = builder.Build();
app.UseWorkflowsApi();
app.Run();
```

`DisableSecurity()` changes the process-wide Elsa API endpoint-security setting. Do not expose a runtime switch that allows a remote caller or a production configuration mistake to toggle it.

## Disable Studio authorization

For a Studio host used only with the unsecured development API, disable the Studio shell's authorization checks:

```csharp
builder.Services.AddShell(options =>
    options.DisableAuthorization = builder.Environment.IsDevelopment());
```

This affects Studio's client-side authorization behavior. It does not disable security on Elsa Server, so configure both hosts consistently for the isolated development environment.

## What this does not disable

* A workflow route exposed by `HttpEndpoint` still follows that activity's `Authorize` and `Policy` settings.
* A reverse proxy, ingress controller, or host-level authorization policy can still reject requests.
* CORS remains a browser-enforced cross-origin control, not an authentication mechanism.

## Before deployment

1. Remove or disable the development-only branch.
2. Configure [Elsa Identity](/guides/authentication/elsa-identity), [Direct OpenID Connect](/guides/authentication/direct-openid-connect), [External Authentication](/guides/authentication/external-authentication), or a [custom scheme](/guides/authentication/custom-authentication).
3. Verify anonymous calls to protected `/elsa/api/*` routes are rejected.
4. Verify Studio receives only the permissions assigned to its signed-in user.
5. Complete the [production hardening](/guides/security/production-hardening) checklist.


# Security & Hardening

Secure Elsa hosts, workflow ingress, bookmark resume URLs, and Studio deployments with focused production hardening guidance.

This section covers the security controls around an Elsa deployment: how traffic reaches it, how workflow callbacks are protected, how sensitive values are handled, and how to operate the deployment safely in production.

Authentication and authorization answer a different question: **who may call Elsa and what may they do?** For Elsa Identity, API keys, OpenID Connect, permissions, and External Authentication, start with [Authentication & Authorization](/guides/authentication).

## Security Guides

| Guide                                                             | Use it when you need to...                                                                                            |
| ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| [HTTP Endpoint Security](/guides/security/http-endpoint-security) | Protect workflow routes created by the `HttpEndpoint` activity, including public endpoints and ASP.NET Core policies. |
| [Bookmark Resume Tokens](/guides/security/bookmark-resume-tokens) | Send or receive tokenized callback URLs that resume waiting workflows.                                                |
| [Production Hardening](/guides/security/production-hardening)     | Configure browser boundaries, ingress, TLS, Studio deployment, monitoring, and operational checks.                    |
| [Secrets Management](/guides/security/secrets-management)         | Store and resolve named values from workflows and Elsa modules.                                                       |

## Scope and Boundaries

Security controls complement authentication; they do not replace it.

* Secure **workflow ingress** separately from Elsa API access. A public `HttpEndpoint` does not make the Elsa API public, and Elsa API permissions do not automatically protect a workflow route. See [HTTP endpoint security](/guides/security/http-endpoint-security).
* Treat a bookmark resume URL as a bearer capability. Give it an appropriate lifetime, do not log its token, and apply controls appropriate to the callback's risk. See [Bookmark resume tokens](/guides/security/bookmark-resume-tokens).
* Keep host and infrastructure secrets outside source control. For values that workflows must resolve, use the [Secrets management](/guides/security/secrets-management) module and protect its encryption key.
* Apply network, transport, browser, and deployment controls before exposing Elsa or Studio to untrusted networks. See [Production hardening](/guides/security/production-hardening).

## Related documentation

* [Authentication & Authorization](/guides/authentication)
* [Permissions reference](/guides/authentication/permissions)
* [Direct OpenID Connect](/guides/authentication/direct-openid-connect)
* [External Authentication](/guides/authentication/external-authentication)
* [Clustering](/guides/clustering)
* [Monitoring & Observability](/operate/monitoring-observability)


# Production Hardening

Harden an Elsa and Elsa Studio deployment with explicit browser boundaries, TLS and proxy controls, monitoring, incident diagnostics, and a production checklist.

Use this guide to prepare an Elsa Server and Elsa Studio deployment for production. It focuses on host, network, browser, and operational controls. Configure identities, permissions, and sign-in providers separately in [Authentication & Authorization](/guides/authentication).

## Browser boundaries: CORS and CSRF

Define a CORS policy for the exact browser origins that need to call the Elsa API. Do not use `AllowAnyOrigin()` in production, and only enable credentials when the application actually uses cookie-based authentication.

```csharp
builder.Services.AddCors(options =>
{
    options.AddPolicy("ElsaCorsPolicy", policy => policy
        .WithOrigins("https://studio.example.com", "https://app.example.com")
        .WithMethods("GET", "POST", "PUT", "DELETE")
        .WithHeaders("Content-Type", "Authorization"));
});

app.UseCors("ElsaCorsPolicy");
```

For a cookie-based browser flow, add `.AllowCredentials()` explicitly and keep the origin allowlist exact. Do not enable credentials for a wildcard origin or for a bearer-token client that does not need cookies.

When cookies are used, configure antiforgery protection and appropriate `SameSite`, `Secure`, and `HttpOnly` cookie settings. Token-based browser clients have different CSRF characteristics, but still require tightly scoped CORS.

Do not use CORS as protection for a server-to-server webhook or bookmark resume URL. Apply caller validation, allowlists where feasible, and rate limits instead. See [Bookmark resume tokens](/guides/security/bookmark-resume-tokens).

## Rate limiting and request boundaries

Apply rate limits at the reverse proxy or ingress so abusive traffic is rejected before it consumes application resources. Use separate policies for:

* public workflow or webhook routes;
* bookmark resume routes; and
* authenticated Elsa API traffic.

Set limits from observed traffic and capacity rather than copying example numbers unchanged. Return `429 Too Many Requests`, monitor the responses, and provide burst capacity only where it cannot be abused.

For `HttpEndpoint` workflow routes, use its request timeout, request-size, file-size, MIME-type, and file-extension settings as applicable. Details are in [HTTP endpoint security](/guides/security/http-endpoint-security).

## TLS, proxies, and network placement

Terminate TLS using trusted certificates, redirect HTTP to HTTPS, and enable HSTS after the public HTTPS configuration is proven. Require TLS 1.2 or later according to your platform policy.

At the proxy boundary:

* forward the original host, client address, and scheme only from trusted proxies;
* configure the host to process forwarded headers correctly, so redirect URLs, cookies, and audit data use the public scheme and host;
* restrict firewall and security-group rules to required ports;
* keep database, cache, and distributed-lock stores off public networks; and
* use network controls or mTLS for sensitive service-to-service paths where required by the threat model.

Workflow runtime operations use persistence and distributed locking; they do not require sticky sessions. If Studio host behavior or another application component requires affinity, scope it to that component rather than treating it as an Elsa runtime requirement. See [Clustering](/guides/clustering).

The [ingress and CORS examples](https://github.com/elsa-workflows/elsa-gitbook/tree/main/guides/security/examples/ingress-cors-snippet.md) are starting points, not a substitute for proxy-vendor hardening guidance.

## Deploy Studio safely

Serve Studio over HTTPS and give it an explicit backend API URL. Register only the production origins and callback URLs required by the Studio host model; do not carry localhost development callbacks into production registrations.

Keep Studio and backend configuration aligned across environments, especially the public URLs, proxy headers, CORS origins, and logout/callback paths. Limit Studio access through the authorization model documented in [Authentication & Authorization](/guides/authentication), and avoid placing secrets in browser-delivered configuration.

For host-model integration details, see [Studio designer integration](/guides/studio/integration).

## Observe and investigate safely

Monitor authentication failures, rejected workflow ingress requests, invalid or repeated bookmark resume attempts, rate-limit responses, workflow cancellations, and unexpected administrative changes. Correlate the events with safe identifiers, source information, and timestamps.

Redact tokens, credentials, personally identifiable information, and sensitive workflow input/output from logs and traces. Establish a retention policy that meets both operational and privacy requirements.

Use [Monitoring & Observability](/operate/monitoring-observability) for tracing and telemetry setup. During an incident, confirm the public route, TLS/proxy headers, CORS response, and rate-limit decision before changing authentication or workflow configuration. General diagnostic steps are in the [Troubleshooting guide](/guides/troubleshooting).

## Production checklist

* [ ] Public Elsa and Studio endpoints enforce HTTPS with trusted certificates.
* [ ] Reverse proxies, forwarded headers, firewall rules, and network exposure have been reviewed for the production topology.
* [ ] CORS allows only required HTTPS origins; cookie-based flows have CSRF and secure-cookie controls.
* [ ] Public workflow and bookmark-resume routes have appropriate validation, rate limits, and allowlists where possible.
* [ ] `HttpEndpoint` request and file limits match the accepted workload.
* [ ] Infrastructure credentials, signing keys, and connection strings are stored outside source control; workflow-resolvable values use [Secrets management](/guides/security/secrets-management) where appropriate.
* [ ] Logs and traces redact credentials, tokens, PII, and sensitive workflow data, with alerting for suspicious failures and rate-limit events.
* [ ] Database and distributed-lock credentials follow least privilege, and production dependencies/images have a patching and vulnerability-review process.
* [ ] The authentication and authorization design has been reviewed using [Authentication & Authorization](/guides/authentication).


# HTTP Endpoint Security

Secure Elsa HTTP workflow endpoints in 3.8.0 by separating workflow ingress authorization from Elsa API permissions and Studio access.

`HttpEndpoint` security in Elsa has two separate layers:

1. Workflow ingress security for routes handled by the `HttpEndpoint` activity, usually under `/workflows/*`.
2. Elsa API security for routes under `/elsa/api/*`, which Studio and automation clients use.

These layers are independent. A public workflow endpoint does not make the Elsa API public, and Elsa API permissions do not secure a workflow endpoint unless the `HttpEndpoint` itself requires authorization.

## How Elsa exposes `HttpEndpoint`

In `release/3.8.0`, the HTTP module combines the configured HTTP base path with the activity path. By default, the base path is `/workflows`, so a workflow path such as `orders/{id}` is exposed as `/workflows/orders/{id}`.

`HttpEndpoint` defaults that matter for security:

* `SupportedMethods` defaults to `GET`.
* `Authorize` defaults to `false`, so the endpoint is public unless you turn authorization on.
* `Policy` is optional and only applies when the endpoint is authorized.

The route, allowed methods, authorization flag, and optional policy are stored in the generated HTTP bookmark payload. Elsa uses that payload both when starting a workflow from a trigger and when resuming a waiting workflow through the same endpoint.

## Public Endpoints

Leave `Authorize` disabled when the endpoint must be callable anonymously, for example:

* public webhooks
* callback URLs protected by their own signed token
* anonymous form posts that perform their own validation

Public endpoints still need normal HTTP hardening. For `HttpEndpoint`, the built-in knobs are:

* `RequestTimeout`
* `RequestSizeLimit`
* `FileSizeLimit`
* `AllowedFileExtensions`
* `BlockedFileExtensions`
* `AllowedMimeTypes`

You should still apply TLS, ingress rate limits, and payload validation in the host application or reverse proxy.

## Authenticated Endpoints

Set `Authorize` to `true` when the caller must be authenticated before the workflow can start or resume.

With `Authorize = true` and no policy configured:

* Elsa requires an authenticated `HttpContext.User`.
* Any authenticated principal is accepted.

This depends on the host having ASP.NET Core authentication and authorization enabled. If the host omits `app.UseAuthentication()` or `app.UseAuthorization()`, the request reaches Elsa as anonymous and the endpoint authorization fails.

## Policy-Based Endpoints

Set both `Authorize = true` and `Policy = "YourPolicyName"` when the endpoint should use a named ASP.NET Core authorization policy.

Elsa evaluates that policy through `IAuthorizationService.AuthorizeAsync(...)` and passes the current workflow as the protected resource. This lets you use standard ASP.NET Core policies based on roles, claims, groups, or custom authorization handlers.

Example host policy:

```csharp
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("WorkflowOperators", policy =>
        policy.RequireAuthenticatedUser()
            .RequireRole("WorkflowOperator"));
});
```

Then configure the `HttpEndpoint` activity with:

* `Authorize`: enabled
* `Policy`: `WorkflowOperators`

## Actual 3.8.0 Response Behavior

One important release-specific detail: in `release/3.8.0`, failed `HttpEndpoint` authorization currently results in `401 Unauthorized` from the HTTP workflows middleware, even when the failure comes from a policy check.

That means these cases all surface as `401` at the workflow endpoint:

* no authenticated user
* an authenticated user that fails the configured policy

If you are troubleshooting a protected endpoint, do not assume `401` means "not logged in" only. It can also mean the named policy rejected the authenticated caller.

## Elsa API Permissions Are Separate

Elsa API endpoints use `permissions` claims, not the `HttpEndpoint` `Authorize` flag.

Common examples:

| Scenario                                        | Typical Elsa API permissions                                                                                                                                                    |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| View workflow definitions                       | `read:workflow-definitions`                                                                                                                                                     |
| Edit workflow definitions                       | `write:workflow-definitions`                                                                                                                                                    |
| Publish or retract definitions                  | `publish:workflow-definitions`, `retract:workflow-definitions`                                                                                                                  |
| View workflow instances and journal-backed data | `read:workflow-instances`                                                                                                                                                       |
| View activity execution summaries               | `read:activity-execution`                                                                                                                                                       |
| Load Studio designer metadata                   | `read:activity-descriptors`, `read:activity-descriptors-options`, `read:expression-descriptors`, `read:storage-drivers`, `read:variable-descriptors`, `read:installed-features` |
| Administer workflow runtime                     | `ManageWorkflowRuntime`                                                                                                                                                         |
| Full Elsa API access                            | `*`                                                                                                                                                                             |

This is why a user can successfully call a protected `HttpEndpoint` and still fail to use Elsa Studio, or vice versa.

## Studio and Operator Troubleshooting

### `404 Not Found` on a workflow route

Check the effective URL first. With default HTTP settings, `Path = orders/{id}` is served from `/workflows/orders/{id}`, not `/orders/{id}`.

### `401 Unauthorized` on `/workflows/...`

In `release/3.8.0`, this usually means one of:

* `Authorize` is enabled and the request is anonymous
* `Authorize` is enabled, a `Policy` is configured, and the authenticated user failed that policy
* the host did not enable ASP.NET Core authentication/authorization middleware

### Studio signs in but cannot load definitions, instances, or designer metadata

That is usually an Elsa API permissions problem, not a workflow ingress problem. Inspect the bearer token or API key identity and confirm it contains the required `permissions` claims for the `/elsa/api/*` endpoints Studio is calling.

### Public webhook works locally but not behind a gateway

Check the reverse proxy or ingress configuration for:

* path rewriting
* request size limits
* TLS termination
* forwarded authentication headers
* rate limiting rules

## Related Guides

* [Authentication & Authorization](/guides/authentication)
* [Security & Hardening](/guides/security)
* [Direct OpenID Connect](/guides/authentication/direct-openid-connect)
* [HTTP Workflows](/guides/http-workflows)


# Bookmark Resume Tokens

Secure Elsa bookmark resume URLs with expirations, single-use bookmarks, revocation procedures, logging, and ingress controls.

Elsa can generate a tokenized URL that resumes a workflow waiting at a bookmark. These URLs are useful for approval links, webhook callbacks, multi-step forms, and external event notifications.

Treat the token as a bearer capability: anyone who can use a valid URL can attempt to resume the associated bookmark. Do not put the full URL or its `t` query-string value in logs, analytics, support tickets, or referrer URLs.

With the default Elsa API prefix, the public resume endpoint is:

```
GET or POST /elsa/api/bookmarks/resume?t=<encrypted-token>
```

The decrypted token payload identifies a bookmark and workflow instance. It does not contain a caller identity, so it is not a substitute for endpoint authentication where the callback needs an authenticated principal.

## Create a time-bounded URL

Generate a bookmark URL with the shortest lifetime that meets the business need. Elsa provides overloads of `GenerateBookmarkTriggerUrl` that accept a `TimeSpan` or an absolute `DateTimeOffset` expiration.

Typical starting points:

| Use case                     | Starting lifetime                     |
| ---------------------------- | ------------------------------------- |
| Third-party webhook callback | 5–15 minutes                          |
| SMS or push approval         | 1–4 hours                             |
| Email approval               | 24–72 hours                           |
| Document-signing process     | 7–30 days, subject to business policy |

Token lifetime is only one layer. Plan for the token to become unusable when the bookmark is consumed or the workflow is cancelled as well.

## Make resumption single-use

For one-time callbacks, create the bookmark with `AutoBurn = true`:

```csharp
var bookmark = context.CreateBookmark(new CreateBookmarkArgs
{
    Name = "ApprovalBookmark",
    Payload = new { ApprovalId = approvalId },
    AutoBurn = true,
    Callback = OnResumeAsync
});
```

After a successful resume, an auto-burning bookmark is removed. A later use of the same URL should not resume a workflow. Elsa's runtime also uses distributed locking while resuming, which protects concurrent attempts; it is not a reason to omit replay controls for a high-risk business operation.

## Revoke a compromised token

Choose a revocation procedure before issuing callback URLs.

* Cancel the workflow when every outstanding bookmark for that instance must stop being usable.
* For an individual bookmark, use the supported application or operational procedure for the storage provider rather than ad-hoc database changes.
* For especially sensitive integrations, keep an application-level deny-list keyed by a safely derived token identifier, and reject it before the resume endpoint is reached.

Do not store the raw token in a revocation list or in logs if a hashed or otherwise derived identifier will meet the operational need.

## Validate input and observe attempts

The resume request's input is still untrusted input. Validate its schema, size, and business constraints in the workflow or host integration before performing an irreversible action.

Record enough information to investigate abuse without recording the token or sensitive request body:

* timestamp and outcome;
* workflow instance and bookmark identifiers after successful, safe resolution;
* source IP and user agent where available; and
* a correlation identifier or safely derived token identifier.

Alert on unusual failure rates, repeated attempts against a single-use bookmark, or sources that cause sustained rate-limit responses.

## Protect the route at ingress

Always expose resume URLs over HTTPS. Apply rate limits before traffic reaches the host, and allowlist known sources when a callback is internal or comes from a fixed third-party address range. Browser CORS does not protect server-to- server webhooks; configure it only for browser clients that actually need it.

For ingress examples and the wider transport controls, see [Production hardening](/guides/security/production-hardening) and [the ingress and CORS examples](https://github.com/elsa-workflows/elsa-gitbook/tree/main/guides/security/examples/ingress-cors-snippet.md).

## Related documentation

* [HTTP endpoint security](/guides/security/http-endpoint-security)
* [Production hardening](/guides/security/production-hardening)
* [Authentication & Authorization](/guides/authentication)


# Secrets Management

Use Elsa's release-backed Secrets module to store, resolve, rotate, and reference sensitive values from workflows and Elsa Studio.

Elsa 3.8 includes an optional Secrets module for named values that workflows and feature modules can resolve at runtime. It gives you a stable technical name, a store and type, lifecycle metadata, and a reference that can be saved in a workflow definition without saving the cleartext value there.

Use this guide when you need to decide:

* whether to use an Elsa-managed encrypted value or a value already supplied by application configuration;
* how to configure the module and its encryption key;
* how to create, rotate, revoke, and test a secret in Elsa Studio; or
* how a workflow activity should reference a secret.

This feature is separate from OIDC client secrets, identity signing keys, and Kubernetes `Secret` objects. Those are host or infrastructure configuration; the module documented here exposes named values to Elsa workflows and modules.

## What the module provides

The Core package exposes the `UseSecrets` module extension. The default service registration includes:

* an encrypted store (`encrypted`) for values managed by Elsa;
* a configuration-backed store (`configuration`) that reads a value from the host configuration without storing that value in Elsa;
* the `Secret` expression type and runtime resolver; and
* HTTP endpoints for management and the Studio picker whose returned secret models expose metadata rather than cleartext values.

The paired Studio package adds a **Secrets** page under the settings menu and a reusable secret picker for activity inputs. Registering the Studio module does not enable the backend feature; the Server and Studio hosts must both have the corresponding feature available.

## Configure the Core module

For a standalone `AddElsa(...)` host, enable the module and configure the encryption key explicitly:

```csharp
using Elsa.Extensions;

var encryptionKey = Convert.FromBase64String(
    builder.Configuration["ELSA_SECRETS_ENCRYPTION_KEY_BASE64"]
    ?? throw new InvalidOperationException("Missing Elsa secrets encryption key."));

builder.Services.AddElsa(elsa =>
{
    elsa.UseSecrets(secrets =>
    {
        secrets.ConfigureOptions = options =>
        {
            options.EncryptionKey = encryptionKey;
            options.RepositoryFilePath = Path.Combine(
                builder.Environment.ContentRootPath,
                "App_Data",
                "elsa-secrets.json");
        };
    });
});
```

The release's encrypted store uses AES-GCM and accepts an encryption key of exactly 16, 24, or 32 bytes. Keep the key outside source control and keep it stable for as long as encrypted values in the repository must remain readable. Changing or losing it prevents Elsa from resolving the encrypted payloads.

The default repository is a JSON file at:

```
<application base directory>/App_Data/elsa-secrets.json
```

`RepositoryFilePath` changes that location for the default file repository. If you configure a different `ISecretRepository`, use that provider's persistence and migration instructions instead; the file-path option does not configure every persistence provider.

## Choose a store

The release registers two built-in stores. The store is selected when the secret is created and cannot be changed by the update endpoint.

* **`encrypted`** stores a protected payload in the Elsa repository and requires the configured encryption key. The Secrets API supports create, rotate, revoke, delete, and test for this store.
* **`configuration`** stores only the configuration-key metadata. It reads `Elsa:Secrets:<configuration-key>`, then the key itself, and supplies the value from host configuration. The store is read-only from Elsa's perspective.

For example, a configuration-backed secret with configuration key `OrdersDatabase` resolves this host configuration:

```json
{
  "Elsa": {
    "Secrets": {
      "OrdersDatabase": "Host=db;Database=orders;Username=elsa;Password=provided-outside-source-control"
    }
  }
}
```

The configuration store does not copy that value into the Elsa secrets file or database. Use it when deployment infrastructure already owns the secret. Use the encrypted store when the value should be managed through Elsa's secret lifecycle and the host can protect the encryption key.

The release also exposes `ISecretStore` and `ISecretRepository` extension points. The built-in module does not claim to be an integration with Azure Key Vault, AWS Secrets Manager, HashiCorp Vault, or another external vault. Add a custom store/repository or project the external value into application configuration when that is your source of truth.

## Secret identity, types, and lifecycle

### Technical names

The technical name is the reference used by workflows. It is normalized to lowercase and must be 2–200 characters long, start with a letter, and contain only letters, numbers, dots, dashes, underscores, or colons. The release management API lets you edit the display name and description, but it does not provide a rename operation. Choose a stable technical name before publishing workflows.

The default type providers are:

* `text` for passwords, tokens, and connection strings;
* `rsa-key` for RSA key material or a configuration reference; and
* `x509-certificate` for certificate material, a thumbprint, or a configuration reference.

Types constrain which stores can be selected and validate the create/rotate payload. A type is metadata about how a value is interpreted; it does not make a configuration-backed value writable through Elsa.

### Versions and status

Creating a secret creates version 1. Rotating it creates the next version and retires the previously active version. Runtime resolution always selects the latest active, non-expired version. Revoking a secret marks the secret and its active versions as revoked, so resolution stops. Deleting a secret removes the protected payload from the encrypted store and marks the repository record as deleted; deleted records are hidden from normal reads.

The API returns metadata such as the current version and expiration time, not the cleartext value. A successful **Test** operation means the configured store could resolve the latest active version; it does not display the value.

## Reference a secret from a workflow

Store a reference, not a value, in a workflow input or activity property. The reference has a required name and optional type and scope:

```json
{
  "type": "Secret",
  "value": {
    "name": "orders:api-token",
    "typeName": "text",
    "scope": "production"
  }
}
```

At runtime, the `Secret` expression handler resolves the reference through `ISecretResolver`. It validates the optional type and scope, then resolves the latest active version from the selected store. The resolved value is available to the activity while it runs; the serialized workflow definition and workflow state retain the reference rather than the cleartext value.

In code, create the expression explicitly when an activity input supports expressions:

```csharp
using Elsa.Secrets.Expressions;
using Elsa.Secrets.Models;

var authorization = SecretExpression.Create(
    new SecretReference("orders:api-token", SecretTypeNames.Text, "production"));
```

In Studio, the Secrets module contributes the `Secret` expression descriptor with the `secret-picker` UI hint. When an activity input exposes that hint, Studio lists compatible active secrets and can create one inline when the backend reports a writable store. The picker can be constrained by type, store, scope, and whether inline creation is allowed through the input's UI metadata.

## Use the Studio page

After the backend and Studio modules are available, open the **Secrets** page at `/security/secrets`. The release UI supports this workflow:

1. Select **Create Secret** and choose a technical name, type, and store.
2. Enter either a value for the encrypted store or a configuration key for the configuration store.
3. Open the secret to edit its display name/description, rotate it, test resolution, or revoke it.
4. Use a compatible activity property in the workflow designer and select the secret from the picker.

The page displays name, type, store, status, current version, scope, and expiration metadata. It intentionally does not provide a cleartext reveal operation. The Studio module is registered in a custom host with:

```csharp
builder.Services.AddSecretsModule(backendApiConfig);
```

The built-in release hosts register this module for Server, WASM, and custom-elements Studio hosts. A custom host must also register the matching backend API client configuration.

## API and permissions

The route paths below are relative to the Elsa API base path. The endpoint classes in the release use these permissions:

| Operation                                  | Permission       |
| ------------------------------------------ | ---------------- |
| List, read, descriptors, and picker        | `read:secrets`   |
| Create, update details, rotate, and revoke | `write:secrets`  |
| Test resolution                            | `test:secrets`   |
| Delete                                     | `delete:secrets` |

The corresponding routes are:

* Read: `GET /secrets`, `GET /secrets/{name}`, `GET /secrets/descriptors`, and `POST /secrets/picker`.
* Write: `POST /secrets`, `POST /secrets/{name}`, `POST /secrets/{name}/rotate`, and `POST /secrets/{name}/revoke`.
* Test: `POST /secrets/{name}/test`.
* Delete: `DELETE /secrets/{name}`.

Grant the smallest set that matches the user or service's job. A workflow designer who only needs to select existing secrets needs the read path and the appropriate workflow-definition permissions; creating or rotating secrets is a separate administrative capability. See [Elsa API Permissions](/guides/authentication/permissions) for the broader permission model.

## Operational checklist

* Keep the encrypted-store key in a deployment secret manager, not in the repository or a workflow definition.
* Back up the encrypted repository and its encryption key together. A backup without the key is not a usable backup.
* Prefer the configuration store when an external deployment system already owns the value and its rotation process.
* Use scopes such as `production` or `staging` when the same technical name must resolve differently by environment or integration boundary.
* Rotate before expiry, then verify the new version with **Test** and a safe workflow execution path.
* Do not log resolved values, include them in activity output, or copy them into ordinary workflow variables unnecessarily.

For OIDC client secrets and other host authentication settings, use the [authentication](/guides/authentication) and [direct OpenID Connect](/guides/authentication/direct-openid-connect) guides. For deployment-managed Kubernetes or cloud secrets, use the deployment guides rather than the Elsa Secrets API.


# Deployment

This section covers deploying Elsa Workflows to various environments and platforms.

## Guides in This Section

* [Configuration Management](/guides/deployment/configuration-management) - Source-backed map of Elsa Server, Studio, and modular host configuration sections, overrides, and common pitfalls.
* [Kubernetes Basics](/guides/deployment/kubernetes) - Quick-start guide for deploying Elsa to Kubernetes with PostgreSQL persistence, including troubleshooting and production best practices.

## Related Documentation

* [Kubernetes Deployment (Full Guide)](/guides/kubernetes-deployment) - Comprehensive Kubernetes deployment guide with Helm charts, autoscaling, and monitoring
* [Clustering](/guides/clustering) - Multi-node deployment patterns
* [Security & Hardening](/guides/security) - Securing your deployments
* [Database Configuration](/getting-started/database-configuration) - Persistence setup


# Configuration Management

Manage Elsa Server, Elsa Studio, and modular host configuration with appsettings, environment overrides, and source-backed section names for release 3.8.0.

This guide shows how Elsa 3.8 hosts read configuration, which section names they actually bind in the release source, and where environment overrides fit. It focuses on the shipped host patterns in `release/3.8.0`:

* `Elsa.Server.Web` in `elsa-core`
* `Elsa.ModularServer.Web` in `elsa-core`
* `Elsa.Studio.Host.Server` and `Elsa.Studio.Host.Wasm` in `elsa-studio`

Use this guide when you need to answer questions such as:

* Which settings belong in `appsettings.json` versus code?
* Which keys can I override with environment variables?
* Why did a setting not change anything?
* Which settings must stay aligned across Elsa Server, Studio, and my reverse proxy?

## How Elsa Reads Configuration

Elsa does not add a custom global configuration system on top of ASP.NET Core. The shipped server hosts start with `WebApplication.CreateBuilder(args)`, and the WASM Studio host starts with `WebAssemblyHostBuilder.CreateDefault(args)`. After that, each host binds specific configuration sections into options inside `Program.cs`.

That means two things:

1. A setting only works if the host or feature actually binds it.
2. Most overrides follow normal .NET configuration rules, including `appsettings.json`, environment-specific JSON files, user secrets, command line arguments, and environment variables for server-side hosts.

Extension package manifests describe settings for tooling and operators, but they do not bind configuration or provision infrastructure. For the `elsa-package.json` contract and a modular-shell mapping example, see [Package manifests for extensions](/guides/plugins-modules/package-manifests).

For named values resolved by workflows, see [Secrets management](/guides/security/secrets-management). Elsa's configuration-backed secret store reads from host configuration; it is distinct from the deployment-managed environment variables, user secrets, or external vaults that supply those values.

## The Three Main Host Shapes

### 1. Standalone Elsa Server (`AddElsa(...)`)

The sample `Elsa.Server.Web` host binds these release-backed sections:

| Section                                                         | Used for                                                                            |
| --------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `Http`                                                          | HTTP activity base URL, base path, content types, and related HTTP activity options |
| `Identity`                                                      | configuration-based users, applications, roles, and token settings                  |
| `Identity:Tokens`                                               | signing key and token lifetime settings                                             |
| `Multitenancy`                                                  | configuration-based tenants                                                         |
| `IngressRateLimiting`                                           | fixed-window rate limit settings for Elsa APIs and HTTP workflow ingress            |
| `Scripting:CSharp`                                              | C# expression engine options                                                        |
| `Scripting:Python`                                              | Python engine options such as DLL path and scripts                                  |
| `DistributedRuntime:AllowLocalLockProviderInDistributedRuntime` | development/single-host override for distributed runtime locking                    |

The important constraint is that this host does **not** use a single `Elsa:*` root. The sections above are bound explicitly in code.

### 2. Elsa Studio Hosts

The standalone Studio hosts bind a different set of sections:

| Section                        | Used for                                                                  |
| ------------------------------ | ------------------------------------------------------------------------- |
| `Backend`                      | Elsa Server API base URL                                                  |
| `Authentication:Provider`      | selects `ElsaIdentity`, `OpenIdConnect`, or `ElsaLogin` depending on host |
| `Authentication:OpenIdConnect` | OIDC authority, client IDs, scopes, and token behavior                    |
| `Localization`                 | default culture and supported cultures                                    |
| `Shell`                        | server-hosted Studio shell options                                        |
| `DesignerOptions`              | flowchart designer options such as `UseReactFlow`                         |

Set `Authentication:Provider` explicitly. The defaults differ by host:

* `Elsa.Studio.Host.Server` falls back to `ElsaIdentity` if the setting is missing.
* `Elsa.Studio.Host.Wasm` falls back to `OpenIdConnect` if the setting is missing.

### 3. Modular Elsa Server (`CShells`)

`Elsa.ModularServer.Web` uses shell-based configuration instead of the standalone `AddElsa(...)` pattern. Its primary sections are:

| Section                                     | Used for                                          |
| ------------------------------------------- | ------------------------------------------------- |
| `CShells:Shells:*`                          | per-shell feature enablement and feature settings |
| `Diagnostics:OpenTelemetry:Exporter`        | OTLP endpoint and protocol                        |
| `Nuplane`                                   | package feeds and autoload behavior               |
| `Elsa:PlatformIntegration:ShellOverlayPath` | optional JSON overlay file path loaded at startup |

In this host, persistence, authentication, HTTP, secrets, and routing settings live under shell feature names such as `Identity`, `FastEndpoints`, `SqliteWorkflowPersistence`, `Http`, or `Secrets`.

## Configuration Map by App Type

### Standalone Elsa Server example

The `Elsa.Server.Web` sample app ships with settings shaped like this:

```json
{
  "Http": {
    "BaseUrl": "https://localhost:5001",
    "BasePath": "/workflows"
  },
  "Identity": {
    "Tokens": {
      "SigningKey": "CHANGE_ME_TO_A_SECURE_RANDOM_KEY"
    }
  },
  "Multitenancy": {
    "Tenants": []
  },
  "IngressRateLimiting": {
    "Enabled": false
  }
}
```

Use this shape when you host Elsa directly in your own ASP.NET Core app with `builder.Services.AddElsa(...)`.

#### Identity signing key rules in 3.8.0

`Identity:Tokens:SigningKey` is validated in the released server code:

* it is required outside the `Development` and `Demo` environments
* it must not contain leading or trailing whitespace
* it must use printable ASCII characters only
* it must be at least 32 characters long
* known sample values such as `CHANGE_ME_TO_A_SECURE_RANDOM_KEY` are rejected outside development-oriented environments

### Elsa Studio example

The standalone Studio hosts expect settings shaped like this:

```json
{
  "Backend": {
    "Url": "https://localhost:5001/elsa/api"
  },
  "Authentication": {
    "Provider": "OpenIdConnect",
    "OpenIdConnect": {
      "Authority": "https://login.microsoftonline.com/<tenant>/v2.0",
      "ClientId": "<client-id>"
    }
  },
  "Localization": {
    "DefaultCulture": "en-US",
    "SupportedCultures": [ "en-US" ]
  },
  "DesignerOptions": {
    "UseReactFlow": false
  }
}
```

For Blazor WebAssembly, these values usually live in `wwwroot/appsettings.json`. Browser clients do not read server environment variables directly, so production overrides usually happen during build, publish, or host-page generation.

#### OIDC defaults in 3.8.0 Studio hosts

When `Authentication:Provider` is `OpenIdConnect`, the released Studio hosts apply a few defaults that are easy to miss:

* if `AuthenticationScopes` is empty, Studio restores `openid`, `profile`, and `offline_access`
* `GetClaimsFromUserInfoEndpoint` defaults to `false`
* the Blazor Server host defaults callback paths to `/signin-oidc` and `/signout-callback-oidc` when you do not set them explicitly

### Modular server example

The modular server sample uses shell feature configuration like this:

```json
{
  "CShells": {
    "Shells": {
      "Default": {
        "Features": {
          "Identity": {
            "SigningKey": "CHANGE_ME_TO_A_SECURE_RANDOM_KEY"
          },
          "FastEndpoints": {
            "GlobalRoutePrefix": "elsa/api"
          },
          "SqliteWorkflowPersistence": {
            "ConnectionString": "Data Source=elsa_workflows.db;Cache=Shared"
          },
          "Http": {
            "HttpActivityOptions": {
              "BaseUrl": "https://localhost:5001"
            }
          }
        }
      }
    }
  }
}
```

Use this model when you need multiple shells, per-shell routing, or feature configuration that can vary by shell.

## Environment Variables and Overrides

For server-side hosts, normal .NET environment variable mapping applies:

* `:` in configuration paths becomes `__`
* keys are case-insensitive on the .NET side
* later providers override earlier providers

Examples:

```bash
Http__BaseUrl=https://workflows.example.com
Http__BasePath=/workflows
Identity__Tokens__SigningKey=<strong-random-key>
IngressRateLimiting__Enabled=true
IngressRateLimiting__ApiPermitLimit=300
Backend__Url=https://workflows.example.com/elsa/api
Authentication__Provider=OpenIdConnect
ConnectionStrings__PostgreSql=Host=db;Database=elsa;Username=elsa;Password=secret
```

Prefer environment variables, user secrets, or a secret manager for:

* signing keys
* API keys and client secrets
* database credentials
* secrets encryption keys

Do not commit production secrets into `appsettings.json`.

## Settings That Must Stay Aligned

### Public URLs

If Elsa is exposed behind a reverse proxy or ingress, keep these consistent:

* `Http:BaseUrl` should reflect the public Elsa Server URL.
* `Backend:Url` in Studio should point to the public Elsa API URL, usually ending in `/elsa/api`.
* Reverse proxy rules must preserve the same public API path that Studio uses.

If these drift apart, generated links, callbacks, or Studio API calls can fail even when the application itself starts correctly.

### HTTP workflow base path versus Elsa API path

These are separate concerns:

* `Http:BasePath` controls where `HttpEndpoint` activity routes are mounted.
* Elsa API endpoints use `ApiEndpointOptions.RoutePrefix`, whose default is `elsa/api`.

In 3.8, `HttpActivityOptions.ApiRoutePrefix` is obsolete. Do not treat `Http:ApiRoutePrefix` as the source of truth for Elsa API routing in new host code.

### Authentication mode

Studio and Server must agree on the authentication story:

* If Studio uses `OpenIdConnect`, Elsa Server must trust the same identity provider and scopes.
* If Studio uses `ElsaIdentity` or `ElsaLogin`, Elsa Server must expose the corresponding identity endpoints and configured users or applications.

## Migrations and Persistence Toggles

Database connection strings alone do not switch persistence providers. The host must also call the provider-specific Elsa persistence configuration methods.

For EF Core persistence, automatic migrations are controlled on the persistence features through `RunMigrations`, not through a built-in global `Elsa:AutoMigrate` setting.

That distinction matters because:

* changing only `ConnectionStrings:*` does not move Elsa off in-memory stores
* migration behavior belongs to the configured persistence feature
* different hosts can expose migration toggles differently

See [Database Configuration](/getting-started/database-configuration) and [EF Core Migrations](/guides/persistence/ef-migrations) for provider-specific examples.

## Common Pitfalls

### A config key exists in docs but nothing reads it

Check the host's `Program.cs`. Elsa settings are often bound explicitly, so an unbound section does nothing.

### Studio can load, but API calls fail

Check `Backend:Url`, the API route prefix, CORS, and auth provider alignment before debugging the workflow engine itself.

### WASM settings do not change after updating server environment variables

Blazor WASM reads client configuration from static assets such as `wwwroot/appsettings.json`. Updating server environment variables alone does not rewrite those files.

### Reverse proxy deployments generate wrong absolute links

Set `Http:BaseUrl` to the public URL seen by clients, not only the internal container or pod address.

### Distributed runtime warns about local locking

If you enable the distributed runtime and keep a local-only lock provider, Elsa logs a warning because that setup does not coordinate across nodes. Treat `DistributedRuntime:AllowLocalLockProviderInDistributedRuntime=true` as a single-host acknowledgement for development or tests, not as a clustering solution.

## Related Guides

* [Database Configuration](/getting-started/database-configuration)
* [Authentication & Authorization](/guides/authentication)
* [Security & Hardening](/guides/security)
* [Deployment](/guides/deployment)
* [Studio Integration](/guides/studio/integration)


# Kubernetes Basics

Quick start guide for deploying Elsa Workflows to Kubernetes with PostgreSQL persistence, including configuration, troubleshooting, and production best practices.

This guide provides a practical introduction to deploying Elsa Workflows on Kubernetes with PostgreSQL persistence. It focuses on common deployment patterns and configuration challenges, helping you move from SQLite (development) to PostgreSQL (production).

{% hint style="info" %}
**Note:** For comprehensive Kubernetes deployment documentation including Helm charts, autoscaling, monitoring, and service mesh integration, see the [Full Kubernetes Deployment Guide](/guides/kubernetes-deployment).
{% endhint %}

## Overview

This guide covers:

* Using Kubernetes manifests from the elsa-core repository
* Switching from SQLite to PostgreSQL for production
* Common configuration pitfalls and how to avoid them
* Troubleshooting database connectivity and persistence

## Prerequisites

* Kubernetes cluster (v1.24+) - Minikube, k3s, or cloud provider (EKS, AKS, GKE)
* `kubectl` CLI configured to access your cluster
* Basic understanding of Kubernetes concepts (Pods, Services, ConfigMaps, Secrets)
* PostgreSQL database (managed or self-hosted)

## Understanding the elsa-core Kubernetes Manifests

The elsa-core repository includes sample Kubernetes manifests in the `scripts/` directory (if available). These manifests provide a starting point for deploying Elsa Server and Studio to Kubernetes.

### Typical Manifest Structure

```
elsa-core/
├── scripts/
│   ├── kubernetes/
│   │   ├── elsa-server-deployment.yaml
│   │   ├── elsa-server-service.yaml
│   │   ├── elsa-studio-deployment.yaml
│   │   ├── configmap.yaml
│   │   └── secrets.yaml (template)
```

{% hint style="warning" %}
**Note:** The exact structure may vary by version. Always refer to the latest elsa-core repository for current manifest examples. If manifests are not present, use the examples in this guide as a starting point.
{% endhint %}

### What the Manifests Provide

* **elsa-server-deployment.yaml**: Deployment for Elsa Server (workflow runtime and API)
* **elsa-studio-deployment.yaml**: Deployment for Elsa Studio (designer UI)
* **services.yaml**: ClusterIP or LoadBalancer services for external access
* **configmap.yaml**: Application configuration (connection strings, feature flags)
* **secrets.yaml**: Sensitive data (database passwords, API keys)

## Default Configuration: SQLite

By default, Elsa Server deployments often use SQLite for simplicity:

**Default ConfigMap:**

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: elsa-config
data:
  ConnectionStrings__DefaultConnection: "Data Source=/app/data/elsa.db"
  Elsa__Persistence__Provider: "Sqlite"
```

**Why SQLite is Used:**

* Zero configuration required
* Works out of the box
* Suitable for demos and development

**Why SQLite is NOT Suitable for Production:**

* **Single-file database**: Does not support multiple pods (no horizontal scaling)
* **No concurrent writes**: Workflow execution errors under load
* **Data loss risk**: Data is lost if the pod restarts (unless using PersistentVolume)
* **Limited performance**: Not optimized for high-throughput scenarios

{% hint style="danger" %}
**Never use SQLite in production Kubernetes deployments.** Always use PostgreSQL, SQL Server, or MySQL for production workloads.
{% endhint %}

## Switching to PostgreSQL

To use PostgreSQL in Kubernetes, you need to:

1. Deploy or connect to a PostgreSQL database
2. Update connection strings and environment variables
3. Configure Elsa modules to use the PostgreSQL provider
4. Apply database migrations

### Step 1: Deploy PostgreSQL (Optional)

If you don't have an external PostgreSQL instance, deploy one in Kubernetes:

**postgres-deployment.yaml:**

```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:16
        env:
        - name: POSTGRES_DB
          value: elsa_workflows
        - name: POSTGRES_USER
          value: elsa_user
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: password
        ports:
        - containerPort: 5432
        volumeMounts:
        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
      volumes:
      - name: postgres-storage
        persistentVolumeClaim:
          claimName: postgres-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: postgres
spec:
  selector:
    app: postgres
  ports:
  - port: 5432
    targetPort: 5432
  type: ClusterIP
```

**postgres-secret.yaml:**

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: postgres-secret
type: Opaque
stringData:
  password: "your-secure-password-here"  # Change this!
```

Apply the manifests:

```bash
kubectl apply -f postgres-secret.yaml
kubectl apply -f postgres-deployment.yaml
```

{% hint style="info" %}
**Production Recommendation:** Use managed PostgreSQL services (Amazon RDS, Azure Database for PostgreSQL, Google Cloud SQL) instead of self-hosting in Kubernetes for better reliability, automated backups, and reduced operational overhead.
{% endhint %}

### Step 2: Update Elsa Server Configuration

Changing the connection string alone is **not enough**. You must also configure the persistence provider in the Elsa modules.

#### Option A: Environment Variables

Update the Elsa Server deployment to use PostgreSQL:

**elsa-server-deployment.yaml:**

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: elsa-server
spec:
  replicas: 1  # Start with 1 for testing. Production: 3+ for HA and rolling updates
  selector:
    matchLabels:
      app: elsa-server
  template:
    metadata:
      labels:
        app: elsa-server
    spec:
      containers:
      - name: elsa-server
        image: elsaworkflows/elsa-server:latest
        env:
        # Connection string
        - name: ConnectionStrings__PostgreSql
          value: "Host=postgres;Database=elsa_workflows;Username=elsa_user;Password=$(POSTGRES_PASSWORD)"
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: password
        
        # Persistence configuration
        - name: Elsa__Persistence__Provider
          value: "PostgreSql"
        - name: Elsa__Persistence__ConnectionStringName
          value: "PostgreSql"
        
        # Module configuration
        - name: Elsa__Modules__Management__Persistence__Provider
          value: "EntityFrameworkCore.PostgreSql"
        - name: Elsa__Modules__Runtime__Persistence__Provider
          value: "EntityFrameworkCore.PostgreSql"
        
        ports:
        - containerPort: 8080
        - containerPort: 8081
```

#### Option B: ConfigMap and appsettings.json

Mount a ConfigMap as `appsettings.Production.json`:

**elsa-configmap.yaml:**

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: elsa-config
data:
  appsettings.Production.json: |
    {
      "ConnectionStrings": {
        "PostgreSql": "Host=postgres;Database=elsa_workflows;Username=elsa_user;Password=$(POSTGRES_PASSWORD)"
      },
      "Elsa": {
        "Modules": {
          "Management": {
            "Persistence": {
              "Provider": "EntityFrameworkCore.PostgreSql",
              "ConnectionStringName": "PostgreSql"
            }
          },
          "Runtime": {
            "Persistence": {
              "Provider": "EntityFrameworkCore.PostgreSql",
              "ConnectionStringName": "PostgreSql"
            }
          }
        }
      }
    }
```

**Deployment with ConfigMap:**

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: elsa-server
spec:
  template:
    spec:
      containers:
      - name: elsa-server
        image: elsaworkflows/elsa-server:latest
        env:
        - name: ASPNETCORE_ENVIRONMENT
          value: "Production"
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: password
        volumeMounts:
        - name: config
          mountPath: /app/appsettings.Production.json
          subPath: appsettings.Production.json
      volumes:
      - name: config
        configMap:
          name: elsa-config
```

### Step 3: Configure Persistence in Program.cs

If you're building a custom Elsa Server image, configure PostgreSQL persistence in `Program.cs`:

```csharp
using Elsa.Persistence.EFCore.Extensions;
using Elsa.Persistence.EFCore;
using Elsa.Persistence.EFCore.Modules.Management;
using Elsa.Persistence.EFCore.Modules.Runtime;
using Elsa.Extensions;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddElsa(elsa =>
{
    // Configure Management module with PostgreSQL
    elsa.UseWorkflowManagement(management =>
    {
        management.UseEntityFrameworkCore(ef =>
        {
            ef.UsePostgreSql(
                builder.Configuration.GetConnectionString("PostgreSql"),
                new ElsaDbContextOptions
                {
                    MigrationsHistoryTableName = "__EFMigrationsHistory_Management"
                }
            );
        });
    });
    
    // Configure Runtime module with PostgreSQL
    elsa.UseWorkflowRuntime(runtime =>
    {
        runtime.UseEntityFrameworkCore(ef =>
        {
            ef.UsePostgreSql(
                builder.Configuration.GetConnectionString("PostgreSql"),
                new ElsaDbContextOptions
                {
                    MigrationsHistoryTableName = "__EFMigrationsHistory_Runtime"
                }
            );
        });
    });
    
    elsa.UseWorkflowsApi();
    elsa.UseHttp();
});

var app = builder.Build();

app.UseWorkflowsApi();
app.Run();
```

If you want migrations to run at application startup, enable them on the EF Core persistence features you configure rather than relying on a built-in `Elsa:AutoMigrate` setting. Elsa 3.8 wires migration execution through the `RunMigrations` property on the persistence features.

## Why Changing Only the Connection String Isn't Enough

A common mistake is to update the connection string but forget to configure the persistence provider. This leads to:

**Symptoms:**

* Elsa still uses the in-memory stores configured by default
* Connection string is ignored
* Data not persisted to PostgreSQL

**Root Cause:**

Elsa modules use in-memory stores unless you explicitly configure a persistence provider. Simply changing the connection string doesn't change the provider. You must explicitly configure each module:

```csharp
// ❌ Wrong: Only changing connection string
services.AddElsa(elsa =>
{
    elsa.UseWorkflowManagement();  // Uses in-memory stores
    elsa.UseWorkflowRuntime();     // Uses in-memory stores
});

// ✅ Correct: Configure provider for each module
services.AddElsa(elsa =>
{
    elsa.UseWorkflowManagement(management =>
    {
        management.UseEntityFrameworkCore(ef => 
            ef.UsePostgreSql(connectionString));
    });
    
    elsa.UseWorkflowRuntime(runtime =>
    {
        runtime.UseEntityFrameworkCore(ef => 
            ef.UsePostgreSql(connectionString));
    });
});
```

**Configuration Points:**

1. **Connection String**: Specifies where to connect
2. **Provider**: Specifies how to connect (SQLite, PostgreSQL, SQL Server, etc.)
3. **Module Configuration**: Each module (Management, Runtime) needs its own provider configuration

All three must be aligned for PostgreSQL to work.

## Running Database Migrations

Before Elsa Server can use PostgreSQL, the database schema must be created.

### Option 1: Init Container

Use a Kubernetes init container to run migrations before the main app starts:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: elsa-server
spec:
  template:
    spec:
      initContainers:
      - name: migrations
        image: elsaworkflows/elsa-server:latest
        command: ["/bin/sh"]
        args:
        - -c
        - |
          dotnet ef database update --context ManagementElsaDbContext
          dotnet ef database update --context RuntimeElsaDbContext
        env:
        - name: ConnectionStrings__PostgreSql
          value: "Host=postgres;Database=elsa_workflows;Username=elsa_user;Password=$(POSTGRES_PASSWORD)"
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: password
      containers:
      - name: elsa-server
        # ... main container config ...
```

{% hint style="warning" %}
**Important:** The above init container example assumes the Elsa Server image includes the EF Core tooling (`dotnet-ef`). Most production images do **not** include this tool for security and size reasons.

* To run migrations reliably, use a dedicated migration image that includes `dotnet-ef`, or build a custom image for this purpose.
* Alternatively, ensure your main image has the necessary tooling, but this is **not recommended** for production.
  {% endhint %}

### Option 2: Auto-Migration on Startup

Enable auto-migration in the application by setting `RunMigrations` on the EF Core persistence features (simpler but not ideal for production):

```csharp
elsa.UseWorkflowManagement(management =>
{
    management.UseEntityFrameworkCore(ef =>
    {
        ef.UsePostgreSql(builder.Configuration.GetConnectionString("PostgreSql")!);
    });
    management.RunMigrations = true;
});

elsa.UseWorkflowRuntime(runtime =>
{
    runtime.UseEntityFrameworkCore(ef =>
    {
        ef.UsePostgreSql(builder.Configuration.GetConnectionString("PostgreSql")!);
    });
    runtime.RunMigrations = true;
});
```

If you want to switch this behavior per environment, read your own config flag and assign `RunMigrations` from code:

```csharp
var runMigrations = builder.Configuration.GetValue("Persistence:RunMigrations", false);

elsa.UseWorkflowManagement(management =>
{
    management.UseEntityFrameworkCore(ef => ef.UsePostgreSql(builder.Configuration.GetConnectionString("PostgreSql")!));
    management.RunMigrations = runMigrations;
});

elsa.UseWorkflowRuntime(runtime =>
{
    runtime.UseEntityFrameworkCore(ef => ef.UsePostgreSql(builder.Configuration.GetConnectionString("PostgreSql")!));
    runtime.RunMigrations = runMigrations;
});
```

Set the environment variable in the deployment:

```yaml
env:
- name: Persistence__RunMigrations
  value: "true"
```

{% hint style="warning" %}
**Production Best Practice:** Run migrations as a separate Job or init container, not on every pod startup. This prevents race conditions when multiple pods start simultaneously.
{% endhint %}

### Option 3: Kubernetes Job

Create a one-time migration Job:

**elsa-migration-job.yaml:**

```yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: elsa-migrations
spec:
  template:
    spec:
      containers:
      - name: migrations
        image: elsaworkflows/elsa-server:latest
        command: ["/bin/sh", "-c"]
        args:
        - |
          dotnet ef database update --context ManagementElsaDbContext
          dotnet ef database update --context RuntimeElsaDbContext
        env:
        - name: ConnectionStrings__PostgreSql
          value: "Host=postgres;Database=elsa_workflows;Username=elsa_user;Password=$(POSTGRES_PASSWORD)"
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: password
      restartPolicy: OnFailure
```

{% hint style="warning" %}
**Important:** Similar to the init container example, this Job assumes the Elsa Server image includes the EF Core tooling (`dotnet-ef`). Most production images do **not** include this tool for security and size reasons.

* To run migrations reliably, use a dedicated migration image that includes `dotnet-ef`, or build a custom image for this purpose.
* Alternatively, ensure your main image has the necessary tooling, but this is **not recommended** for production.
  {% endhint %}

Run the job before deploying Elsa Server:

```bash
kubectl apply -f elsa-migration-job.yaml
kubectl wait --for=condition=complete job/elsa-migrations --timeout=300s
kubectl apply -f elsa-server-deployment.yaml
```

## Troubleshooting

### Problem: Elsa Still Uses SQLite

**Symptoms:**

* `elsa.db` file created in pod
* PostgreSQL connection string appears in logs but isn't used
* No tables created in PostgreSQL

**Diagnosis:**

```bash
# Check pod logs
kubectl logs -l app=elsa-server --tail=100

# Look for:
# - "Using Sqlite provider" (wrong)
# - "Using PostgreSql provider" (correct)
```

**Fix:**

1. Verify provider configuration in environment variables:

   ```yaml
   env:
   - name: Elsa__Modules__Management__Persistence__Provider
     value: "EntityFrameworkCore.PostgreSql"
   ```
2. Or ensure `appsettings.Production.json` is mounted correctly:

   ```bash
   kubectl exec -it deployment/elsa-server -- cat /app/appsettings.Production.json
   ```
3. Check that the PostgreSQL package is included in your Docker image:

   ```dockerfile
   # In Dockerfile
   RUN dotnet add package Elsa.Persistence.EFCore.PostgreSql
   ```

### Problem: Connection Refused or Timeout

**Symptoms:**

```
Npgsql.NpgsqlException: Connection refused
or
A connection attempt failed because the connected party did not properly respond
```

**Diagnosis:**

```bash
# Check if PostgreSQL pod is running
kubectl get pods -l app=postgres

# Check PostgreSQL service
kubectl get svc postgres

# Test connection from Elsa Server pod
kubectl exec -it deployment/elsa-server -- /bin/sh
apk add postgresql-client
psql -h postgres -U elsa_user -d elsa_workflows
```

**Fix:**

1. Verify PostgreSQL service name matches connection string:

   ```yaml
   # Connection string must use service name
   Host=postgres  # ← Must match service metadata.name
   ```
2. Ensure PostgreSQL is ready before Elsa Server starts:

   ```yaml
   # Add readiness probe to postgres deployment
   readinessProbe:
     exec:
       command: ["pg_isready", "-U", "elsa_user"]
     initialDelaySeconds: 5
     periodSeconds: 5
   ```
3. Check namespace - services in different namespaces require FQDN:

   ```yaml
   # If postgres is in namespace "database"
   Host=postgres.database.svc.cluster.local
   ```

### Problem: Tables Not Created

**Symptoms:**

* PostgreSQL connection succeeds
* No error messages in logs
* Queries fail: "relation 'Elsa\_WorkflowDefinitions' does not exist"

**Diagnosis:**

```bash
# Connect to PostgreSQL
kubectl exec -it deployment/postgres -- psql -U elsa_user -d elsa_workflows

# List tables
\dt

# Expected tables:
# Elsa_WorkflowDefinitions
# Elsa_WorkflowInstances
# Elsa_ActivityExecutionRecords
# ... and others
```

**Fix:**

1. Ensure migrations ran successfully:

   ```bash
   # Check migration job logs
   kubectl logs job/elsa-migrations

   # Look for:
   # "Applying migration '20240101000000_InitialCreate'"
   # "Done."
   ```
2. Manually run migrations if needed:

   ```bash
   kubectl run -it --rm migrations --image=elsaworkflows/elsa-server:latest \
     --restart=Never \
     --env="ConnectionStrings__PostgreSql=Host=postgres;Database=elsa_workflows;Username=elsa_user;Password=..." \
     -- dotnet ef database update
   ```
3. Check migration history:

   ```sql
   SELECT * FROM "__EFMigrationsHistory_Management";
   SELECT * FROM "__EFMigrationsHistory_Runtime";
   ```

### Problem: Missing Environment Variables

**Symptoms:**

* Connection string contains literal `$(POSTGRES_PASSWORD)` instead of actual password
* Authentication failures

**Diagnosis:**

```bash
# Check environment variables in pod
kubectl exec -it deployment/elsa-server -- printenv | grep -i postgres

# Should show:
# POSTGRES_PASSWORD=actual_password_here
# ConnectionStrings__PostgreSql=Host=postgres;...Password=actual_password_here
```

**Fix:**

Environment variable substitution in connection strings doesn't happen automatically. Use one of these approaches:

**Option 1: Reference secret directly in each field:**

```yaml
env:
- name: DB_HOST
  value: "postgres"
- name: DB_NAME
  value: "elsa_workflows"
- name: DB_USER
  value: "elsa_user"
- name: DB_PASSWORD
  valueFrom:
    secretKeyRef:
      name: postgres-secret
      key: password
- name: ConnectionStrings__PostgreSql
  value: "Host=$(DB_HOST);Database=$(DB_NAME);Username=$(DB_USER);Password=$(DB_PASSWORD)"
```

**Option 2: Build connection string in code:**

```csharp
// In Program.cs
var host = builder.Configuration["DB_HOST"];
var database = builder.Configuration["DB_NAME"];
var user = builder.Configuration["DB_USER"];
var password = builder.Configuration["DB_PASSWORD"];

var connectionString = $"Host={host};Database={database};Username={user};Password={password}";

builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowManagement(management =>
    {
        management.UseEntityFrameworkCore(ef => ef.UsePostgreSql(connectionString));
    });
    // ...
});
```

### Problem: ConfigMap Not Mounted

**Symptoms:**

* Settings in ConfigMap not applied
* App uses default configuration

**Diagnosis:**

```bash
# Check if ConfigMap exists
kubectl get configmap elsa-config -o yaml

# Verify file is mounted in pod
kubectl exec -it deployment/elsa-server -- ls -la /app/appsettings.Production.json

# Check file content
kubectl exec -it deployment/elsa-server -- cat /app/appsettings.Production.json
```

**Fix:**

1. Ensure volume mount path is correct:

   ```yaml
   volumeMounts:
   - name: config
     mountPath: /app/appsettings.Production.json  # Must match app's config path
     subPath: appsettings.Production.json
   ```
2. Set `ASPNETCORE_ENVIRONMENT` to load the file:

   ```yaml
   env:
   - name: ASPNETCORE_ENVIRONMENT
     value: "Production"
   ```
3. Verify ConfigMap is in the same namespace as the deployment.

## Verifying PostgreSQL is Being Used

After deploying, confirm that Elsa is using PostgreSQL:

### 1. Check Logs

```bash
kubectl logs -l app=elsa-server --tail=50 | grep -i postgres

# Look for messages like:
# "Using PostgreSQL provider"
# "Executed DbCommand (123ms) [Parameters=[], CommandType='Text']"
```

### 2. Check Database Tables

```bash
kubectl exec -it deployment/postgres -- psql -U elsa_user -d elsa_workflows -c "\dt"

# Expected output:
#                   List of relations
#  Schema |            Name              | Type  |   Owner
# --------+------------------------------+-------+-----------
#  public | Elsa_ActivityExecutionRecords| table | elsa_user
#  public | Elsa_Bookmarks               | table | elsa_user
#  public | Elsa_WorkflowDefinitions     | table | elsa_user
#  public | Elsa_WorkflowInstances       | table | elsa_user
```

### 3. Create a Test Workflow

```bash
# Port-forward Elsa Server
kubectl port-forward svc/elsa-server 8080:80

# Execute an existing workflow definition via API
curl -X POST http://localhost:8080/elsa/api/workflow-definitions/{definitionId}/execute \
  -H "Content-Type: application/json" \
  -d '{
    "input": {
      "message": "Hello from Kubernetes!"
    }
  }'

# Verify workflow instance was persisted
kubectl exec -it deployment/postgres -- psql -U elsa_user -d elsa_workflows \
  -c "SELECT id, status FROM \"Elsa_WorkflowInstances\" ORDER BY created_at DESC LIMIT 1;"
```

## Production Best Practices

### 1. Use Managed PostgreSQL

* **Amazon RDS for PostgreSQL**: Automated backups, point-in-time recovery, Multi-AZ
* **Azure Database for PostgreSQL**: High availability, automatic patching, geo-replication
* **Google Cloud SQL**: Automated backups, read replicas, automatic failover

### 2. Separate Database User Permissions

```sql
-- Create read-only user for monitoring
CREATE USER elsa_readonly WITH PASSWORD 'secure_password';
GRANT CONNECT ON DATABASE elsa_workflows TO elsa_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO elsa_readonly;

-- Create migration user with schema modification rights
CREATE USER elsa_migrations WITH PASSWORD 'secure_password';
GRANT ALL PRIVILEGES ON DATABASE elsa_workflows TO elsa_migrations;

-- Application user with limited permissions
CREATE USER elsa_app WITH PASSWORD 'secure_password';
GRANT CONNECT ON DATABASE elsa_workflows TO elsa_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO elsa_app;
```

### 3. Use Connection Pooling

```yaml
env:
- name: ConnectionStrings__PostgreSql
  value: "Host=postgres;Database=elsa_workflows;Username=elsa_user;Password=$(PASSWORD);Pooling=true;MinPoolSize=1;MaxPoolSize=20"
```

### 4. Enable TLS for Database Connections

```yaml
env:
- name: ConnectionStrings__PostgreSql
  value: "Host=postgres;Database=elsa_workflows;Username=elsa_user;Password=$(PASSWORD);SSL Mode=Require;Trust Server Certificate=false"
```

### 5. Implement Health Checks

```yaml
livenessProbe:
  httpGet:
    path: /health/live
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /health/ready
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 5
```

### 6. Set Resource Limits

```yaml
resources:
  requests:
    memory: "512Mi"
    cpu: "250m"
  limits:
    memory: "2Gi"
    cpu: "1000m"
```

### 7. Production Scaling Considerations

For production deployments, run at least 3 replicas:

```yaml
spec:
  replicas: 3  # Minimum for production
```

**Why 3+ replicas?**

* **High Availability**: If one pod fails, others continue serving traffic
* **Rolling Updates**: Allows zero-downtime deployments (update one pod at a time)
* **Load Distribution**: Better distribution of workflow execution across pods
* **Pod Disruption Budget**: Can configure PDB to maintain minimum available pods

**Scaling Strategy:**

* **Development/Staging**: 1-2 replicas
* **Production (low traffic)**: 3 replicas
* **Production (high traffic)**: 5-10+ replicas with HPA
* **Enterprise**: 10+ replicas with node affinity and pod anti-affinity rules

For automatic scaling based on CPU/memory usage, see the [Full Kubernetes Deployment Guide](/guides/kubernetes-deployment#horizontal-pod-autoscaling).

## Next Steps

* **Scale Your Deployment**: Configure [Horizontal Pod Autoscaling](/guides/kubernetes-deployment#horizontal-pod-autoscaling)
* **Add Monitoring**: Set up [Prometheus and Grafana](/guides/kubernetes-deployment#monitoring-with-prometheus--grafana)
* **Secure Your Cluster**: Configure [authentication and authorization](/guides/authentication)
* **Integrate with Studio**: Set up [Blazor Dashboard](/guides/integration/blazor-dashboard)
* **Production Hardening**: Follow the [Production Checklist](/guides/kubernetes-deployment#production-best-practices)

## Related Documentation

* [Full Kubernetes Deployment Guide](/guides/kubernetes-deployment) - Complete reference with Helm charts, autoscaling, and monitoring
* [Database Configuration](/getting-started/database-configuration) - Detailed persistence setup
* [Clustering Guide](/guides/clustering) - Multi-node deployment patterns
* [Security & Hardening](/guides/security) - Securing your Kubernetes deployment
* [Troubleshooting](/guides/troubleshooting) - Common issues and solutions

***

**Last Updated:** 2025-12-02\
**Addresses Issues:** #75


# Kubernetes Deployment

Complete Kubernetes deployment guide for Elsa Workflows including Helm charts, deployment configurations, ingress setup, autoscaling, monitoring, service mesh integration, and production best practice

This comprehensive guide covers deploying Elsa Workflows to Kubernetes in production environments. Whether you're using managed Kubernetes services (EKS, AKS, GKE) or self-hosted clusters, this guide provides everything you need for a reliable, scalable deployment.

## Overview

Elsa Workflows can be deployed to Kubernetes using either:

* **Helm Charts** (Recommended) - Simplified deployment and management
* **Raw Kubernetes Manifests** - Full control over configuration

This guide covers both approaches and includes:

* Elsa Server and Studio deployments
* Database integration and persistence
* Ingress configuration for external access
* Horizontal Pod Autoscaling (HPA)
* Monitoring with Prometheus and Grafana
* Service mesh integration (Istio/Linkerd)
* Production best practices and troubleshooting

## Table of Contents

* [Prerequisites](#prerequisites)
* [Architecture Overview](#architecture-overview)
* [Helm Chart Deployment](#helm-chart-deployment)
* [Kubernetes Manifest Deployment](#kubernetes-manifest-deployment)
* [Database Configuration](#database-configuration)
* [Ingress Setup](#ingress-setup)
* [Horizontal Pod Autoscaling](#horizontal-pod-autoscaling)
* [Persistent Storage](#persistent-storage)
* [Monitoring with Prometheus & Grafana](#monitoring-with-prometheus--grafana)
* [Service Mesh Integration](#service-mesh-integration)
* [Distributed Configuration](#distributed-configuration)
* [Troubleshooting](#troubleshooting)
* [Production Best Practices](#production-best-practices)

## Prerequisites

Before deploying to Kubernetes, ensure you have:

### Required Tools

* **kubectl** v1.28+ - Kubernetes command-line tool
* **Helm** v3.12+ - Kubernetes package manager (if using Helm charts)
* **Docker** - For building custom images (optional)
* Access to a Kubernetes cluster (v1.28+)

### Cluster Requirements

* **Minimum**: 2 nodes with 4GB RAM and 2 CPU cores each
* **Recommended**: 3+ nodes with 8GB RAM and 4 CPU cores each
* **Storage**: Dynamic volume provisioning support (for databases)
* **Ingress Controller**: NGINX, Traefik, or cloud provider load balancer

### Knowledge Requirements

* Basic Kubernetes concepts (Pods, Services, Deployments)
* Understanding of Elsa architecture (see [Architecture Overview](/getting-started/architecture-overview))
* Familiarity with database configuration
* Basic YAML syntax

{% hint style="info" %}
**New to Kubernetes?**

For local development and testing, consider using [Minikube](https://minikube.sigs.k8s.io/), [k3d](https://k3d.io/), or [Docker Desktop Kubernetes](https://docs.docker.com/desktop/kubernetes/) before deploying to production clusters.
{% endhint %}

## Architecture Overview

A typical Elsa Workflows Kubernetes deployment consists of:

```
┌─────────────────────────────────────────────────────────────┐
│                       Ingress Controller                     │
│                   (NGINX / Traefik / ALB)                    │
└─────────────────┬───────────────────────┬───────────────────┘
                  │                       │
         ┌────────▼──────────┐   ┌───────▼────────┐
         │   Elsa Studio     │   │  Elsa Server   │
         │  (Deployment)     │   │  (Deployment)  │
         │   Replicas: 2+    │   │  Replicas: 3+  │
         └───────────────────┘   └────────┬───────┘
                                          │
                      ┌───────────────────┼───────────────────┐
                      │                   │                   │
             ┌────────▼────────┐ ┌───────▼────────┐ ┌───────▼────────┐
             │   PostgreSQL    │ │     Redis      │ │   RabbitMQ     │
             │  (StatefulSet)  │ │ (StatefulSet)  │ │ (StatefulSet)  │
             │   + PVC         │ │    + PVC       │ │    + PVC       │
             └─────────────────┘ └────────────────┘ └────────────────┘
```

### Components

1. **Elsa Server**: Hosts the workflow engine and REST API
2. **Elsa Studio**: Visual workflow designer (optional, can be separate)
3. **Database**: PostgreSQL, SQL Server, or MySQL (with persistent storage)
4. **Redis**: Distributed caching and locking
5. **RabbitMQ**: Message broker for distributed cache invalidation (via MassTransit)
6. **Ingress**: External access routing
7. **Monitoring**: Prometheus metrics and Grafana dashboards

## Helm Chart Deployment

Helm is the recommended approach for deploying Elsa Workflows to Kubernetes. While official Helm charts are under development, this section provides a production-ready chart configuration.

### Step 1: Create Helm Chart Structure

Create a new Helm chart for Elsa:

```bash
helm create elsa-workflows
cd elsa-workflows
```

### Step 2: Configure Values

Create a `values.yaml` file with the following configuration:

```yaml
# values.yaml - Elsa Workflows Helm Chart Configuration

# Global settings
global:
  imageRegistry: docker.io
  imagePullPolicy: IfNotPresent
  storageClass: ""  # Use default storage class

# Elsa Server configuration
elsaServer:
  enabled: true
  name: elsa-server
  
  image:
    repository: elsaworkflows/elsa-server-v3
    tag: latest
    pullPolicy: IfNotPresent
  
  replicaCount: 3
  
  resources:
    requests:
      memory: "512Mi"
      cpu: "500m"
    limits:
      memory: "2Gi"
      cpu: "2000m"
  
  env:
    - name: ASPNETCORE_ENVIRONMENT
      value: "Production"
    - name: HTTP_PORTS
      value: "8080"
    - name: DATABASEPROVIDER
      value: "PostgreSql"
    - name: CONNECTIONSTRINGS__POSTGRESQL
      valueFrom:
        secretKeyRef:
          name: elsa-secrets
          key: postgresql-connection-string
    - name: REDIS__CONNECTIONSTRING
      valueFrom:
        secretKeyRef:
          name: elsa-secrets
          key: redis-connection-string
    - name: RABBITMQ__CONNECTIONSTRING
      valueFrom:
        secretKeyRef:
          name: elsa-secrets
          key: rabbitmq-connection-string
  
  service:
    type: ClusterIP
    port: 80
    targetPort: 8080
  
  autoscaling:
    enabled: true
    minReplicas: 3
    maxReplicas: 10
    targetCPUUtilizationPercentage: 70
    targetMemoryUtilizationPercentage: 80
  
  livenessProbe:
    httpGet:
      path: /health/live
      port: 8080
    initialDelaySeconds: 30
    periodSeconds: 10
    timeoutSeconds: 5
    failureThreshold: 3
  
  readinessProbe:
    httpGet:
      path: /health/ready
      port: 8080
    initialDelaySeconds: 20
    periodSeconds: 5
    timeoutSeconds: 3
    failureThreshold: 3

# Elsa Studio configuration
elsaStudio:
  enabled: true
  name: elsa-studio
  
  image:
    repository: elsaworkflows/elsa-studio-v3
    tag: latest
    pullPolicy: IfNotPresent
  
  replicaCount: 2
  
  resources:
    requests:
      memory: "256Mi"
      cpu: "250m"
    limits:
      memory: "1Gi"
      cpu: "1000m"
  
  env:
    - name: ASPNETCORE_ENVIRONMENT
      value: "Production"
    - name: HTTP_PORTS
      value: "8080"
    - name: ELSASERVER__URL
      value: "http://elsa-server/elsa/api"
  
  service:
    type: ClusterIP
    port: 80
    targetPort: 8080
  
  autoscaling:
    enabled: true
    minReplicas: 2
    maxReplicas: 5
    targetCPUUtilizationPercentage: 75

# PostgreSQL configuration
postgresql:
  enabled: true
  auth:
    username: elsa
    password: ""  # Set via secret
    database: elsa
  
  primary:
    persistence:
      enabled: true
      size: 50Gi
      storageClass: ""  # Use default
    
    resources:
      requests:
        memory: "1Gi"
        cpu: "500m"
      limits:
        memory: "4Gi"
        cpu: "2000m"
    
    initdb:
      scripts:
        init.sql: |
          CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
          CREATE EXTENSION IF NOT EXISTS "pg_trgm";

# Redis configuration (for distributed locking and caching)
redis:
  enabled: true
  architecture: standalone
  auth:
    enabled: true
    password: ""  # Set via secret
  
  master:
    persistence:
      enabled: true
      size: 10Gi
    
    resources:
      requests:
        memory: "256Mi"
        cpu: "250m"
      limits:
        memory: "1Gi"
        cpu: "1000m"

# RabbitMQ configuration (for MassTransit)
rabbitmq:
  enabled: true
  auth:
    username: elsa
    password: ""  # Set via secret
  
  persistence:
    enabled: true
    size: 20Gi
  
  resources:
    requests:
      memory: "512Mi"
      cpu: "500m"
    limits:
      memory: "2Gi"
      cpu: "1000m"
  
  replicaCount: 3
  clustering:
    enabled: true

# Ingress configuration
ingress:
  enabled: true
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
  
  hosts:
    - host: studio.example.com
      paths:
        - path: /
          pathType: Prefix
          service: elsa-studio
    - host: api.example.com
      paths:
        - path: /
          pathType: Prefix
          service: elsa-server
  
  tls:
    - secretName: elsa-tls
      hosts:
        - studio.example.com
        - api.example.com

# Monitoring configuration
monitoring:
  enabled: true
  serviceMonitor:
    enabled: true
    interval: 30s
  
  grafana:
    enabled: true
    dashboards:
      enabled: true

# Security settings
securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  fsGroup: 1000
  capabilities:
    drop:
      - ALL

# Pod disruption budget
podDisruptionBudget:
  enabled: true
  minAvailable: 1

# Network policies
networkPolicy:
  enabled: true
  policyTypes:
    - Ingress
    - Egress
```

### Step 3: Create Secrets

Create a Kubernetes secret for sensitive configuration:

```bash
export DB_PASSWORD='<your_postgres_password>'
export REDIS_PASSWORD='<your_redis_password>'
export RABBITMQ_PASSWORD='<your_rabbitmq_password>'
kubectl create secret generic elsa-secrets \
  --from-literal=postgresql-connection-string="Server=elsa-postgresql;Username=elsa;Database=elsa;Port=5432;Password=${DB_PASSWORD};SSLMode=Require;MaxPoolSize=100" \
  --from-literal=redis-connection-string="elsa-redis-master:6379,password=${REDIS_PASSWORD},ssl=False,abortConnect=False" \
  --from-literal=rabbitmq-connection-string="amqp://elsa:${RABBITMQ_PASSWORD}@elsa-rabbitmq:5672/" \
  --namespace elsa-workflows
```

{% hint style="warning" %}
**Security Best Practice**

Never commit secrets to version control. Use external secret management tools like:

* Sealed Secrets
* External Secrets Operator
* HashiCorp Vault
* Cloud provider secret managers (AWS Secrets Manager, Azure Key Vault, GCP Secret Manager)
  {% endhint %}

### Step 4: Install with Helm

```bash
# Add required Helm repositories
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update

# Create namespace
kubectl create namespace elsa-workflows

# Install Elsa Workflows
# Store passwords in a secure values file (e.g., secrets.yaml) and never commit it to source control.
helm install elsa-workflows ./elsa-workflows \
  --namespace elsa-workflows \
  --values values.yaml \
  --values secrets.yaml  # Contains sensitive values, never committed
```

### Step 5: Verify Deployment

```bash
# Check deployment status
kubectl get pods -n elsa-workflows

# View logs
kubectl logs -n elsa-workflows -l app=elsa-server --tail=50

# Check services
kubectl get svc -n elsa-workflows

# Verify ingress
kubectl get ingress -n elsa-workflows
```

### Upgrading

To upgrade your deployment:

```bash
# Update values in values.yaml, then:
helm upgrade elsa-workflows ./elsa-workflows \
  --namespace elsa-workflows \
  --values values.yaml

# Check rollout status
kubectl rollout status deployment/elsa-server -n elsa-workflows
```

### Uninstalling

```bash
helm uninstall elsa-workflows --namespace elsa-workflows
```

## Kubernetes Manifest Deployment

For full control over your deployment, you can use raw Kubernetes manifests. This section provides production-ready YAML configurations.

### Directory Structure

```
k8s/
├── namespace.yaml
├── secrets.yaml
├── configmaps.yaml
├── postgresql/
│   ├── statefulset.yaml
│   ├── service.yaml
│   └── pvc.yaml
├── redis/
│   ├── statefulset.yaml
│   └── service.yaml
├── rabbitmq/
│   ├── statefulset.yaml
│   └── service.yaml
├── elsa-server/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── hpa.yaml
│   └── pdb.yaml
├── elsa-studio/
│   ├── deployment.yaml
│   ├── service.yaml
│   └── hpa.yaml
└── ingress.yaml
```

### Namespace

```yaml
# namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: elsa-workflows
  labels:
    name: elsa-workflows
    environment: production
```

### ConfigMap

```yaml
# configmaps.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: elsa-config
  namespace: elsa-workflows
data:
  ASPNETCORE_ENVIRONMENT: "Production"
  HTTP_PORTS: "8080"
  DATABASEPROVIDER: "PostgreSql"
  # Add non-sensitive configuration here
```

### Secrets

```yaml
# secrets.yaml
# DO NOT commit this file with actual values!
# Use kubectl create secret or external secret management
apiVersion: v1
kind: Secret
metadata:
  name: elsa-secrets
  namespace: elsa-workflows
type: Opaque
stringData:
  postgresql-connection-string: "Server=elsa-postgresql;Username=elsa;Database=elsa;Port=5432;Password=CHANGE_ME;SSLMode=Prefer;MaxPoolSize=100"
  redis-connection-string: "elsa-redis:6379,password=CHANGE_ME,ssl=False,abortConnect=False"
  rabbitmq-connection-string: "amqp://elsa:CHANGE_ME@elsa-rabbitmq:5672/"
```

### Elsa Server Deployment

```yaml
# elsa-server/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: elsa-server
  namespace: elsa-workflows
  labels:
    app: elsa-server
    component: api
    version: v3.7.0
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: elsa-server
  template:
    metadata:
      labels:
        app: elsa-server
        component: api
        version: v3.7.0
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "8080"
        prometheus.io/path: "/metrics"
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        fsGroup: 1000
      
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values:
                        - elsa-server
                topologyKey: kubernetes.io/hostname
      
      containers:
        - name: elsa-server
          image: elsaworkflows/elsa-server-v3:latest
          imagePullPolicy: IfNotPresent
          
          ports:
            - name: http
              containerPort: 8080
              protocol: TCP
          
          env:
            - name: ASPNETCORE_ENVIRONMENT
              value: "Production"
            - name: HTTP_PORTS
              value: "8080"
            - name: DATABASEPROVIDER
              value: "PostgreSql"
            - name: CONNECTIONSTRINGS__POSTGRESQL
              valueFrom:
                secretKeyRef:
                  name: elsa-secrets
                  key: postgresql-connection-string
            - name: REDIS__CONNECTIONSTRING
              valueFrom:
                secretKeyRef:
                  name: elsa-secrets
                  key: redis-connection-string
            - name: RABBITMQ__CONNECTIONSTRING
              valueFrom:
                secretKeyRef:
                  name: elsa-secrets
                  key: rabbitmq-connection-string
            # Distributed runtime configuration
            - name: ELSA__RUNTIME__TYPE
              value: "Distributed"
            - name: ELSA__CACHING__TYPE
              value: "Distributed"
          
          resources:
            requests:
              memory: "512Mi"
              cpu: "500m"
            limits:
              memory: "2Gi"
              cpu: "2000m"
          
          livenessProbe:
            httpGet:
              path: /health/live
              port: http
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 3
          
          readinessProbe:
            httpGet:
              path: /health/ready
              port: http
            initialDelaySeconds: 20
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 3
          
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 15"]
          
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop:
                - ALL
            readOnlyRootFilesystem: false  # Set to true if application supports it
            # If readOnlyRootFilesystem: true, mount volumes for writable paths:
            # volumeMounts:
            #   - name: tmp
            #     mountPath: /tmp
```

### Elsa Server Service

```yaml
# elsa-server/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: elsa-server
  namespace: elsa-workflows
  labels:
    app: elsa-server
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: http
      protocol: TCP
      name: http
  selector:
    app: elsa-server
```

### Elsa Studio Deployment

```yaml
# elsa-studio/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: elsa-studio
  namespace: elsa-workflows
  labels:
    app: elsa-studio
    component: ui
    version: v3.7.0
spec:
  replicas: 2
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: elsa-studio
  template:
    metadata:
      labels:
        app: elsa-studio
        component: ui
        version: v3.7.0
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        fsGroup: 1000
      
      containers:
        - name: elsa-studio
          image: elsaworkflows/elsa-studio-v3:latest
          imagePullPolicy: IfNotPresent
          
          ports:
            - name: http
              containerPort: 8080
              protocol: TCP
          
          env:
            - name: ASPNETCORE_ENVIRONMENT
              value: "Production"
            - name: HTTP_PORTS
              value: "8080"
            - name: ELSASERVER__URL
              value: "http://elsa-server/elsa/api"
          
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "1Gi"
              cpu: "1000m"
          
          livenessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 20
            periodSeconds: 10
          
          readinessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 10
            periodSeconds: 5
          
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop:
                - ALL
```

### Elsa Studio Service

```yaml
# elsa-studio/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: elsa-studio
  namespace: elsa-workflows
  labels:
    app: elsa-studio
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: http
      protocol: TCP
      name: http
  selector:
    app: elsa-studio
```

### Deploy All Manifests

```bash
# Create namespace
kubectl apply -f k8s/namespace.yaml

# Create secrets (use environment-specific values)
kubectl apply -f k8s/secrets.yaml

# Deploy infrastructure (database, cache, message broker)
kubectl apply -f k8s/postgresql/
kubectl apply -f k8s/redis/
kubectl apply -f k8s/rabbitmq/

# Wait for infrastructure to be ready
kubectl wait --for=condition=ready pod -l app=postgresql -n elsa-workflows --timeout=300s

# Deploy Elsa components
kubectl apply -f k8s/elsa-server/
kubectl apply -f k8s/elsa-studio/

# Configure ingress
kubectl apply -f k8s/ingress.yaml
```

## Database Configuration

Proper database configuration is crucial for production Kubernetes deployments. This section covers PostgreSQL, SQL Server, and MySQL configurations.

### PostgreSQL StatefulSet

```yaml
# postgresql/statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: elsa-postgresql
  namespace: elsa-workflows
spec:
  serviceName: elsa-postgresql
  replicas: 1  # Use 3+ for HA with replication
  selector:
    matchLabels:
      app: postgresql
  template:
    metadata:
      labels:
        app: postgresql
    spec:
      containers:
        - name: postgresql
          image: postgres:16-alpine
          
          ports:
            - containerPort: 5432
              name: postgres
          
          env:
            - name: POSTGRES_DB
              value: "elsa"
            - name: POSTGRES_USER
              value: "elsa"
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: elsa-secrets
                  key: postgres-password
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
            - name: POSTGRES_INITDB_ARGS
              value: "--encoding=UTF8 --lc-collate=en_US.utf8 --lc-ctype=en_US.utf8"
          
          args:
            - "-c"
            - "max_connections=200"
            - "-c"
            - "shared_buffers=256MB"
            - "-c"
            - "effective_cache_size=1GB"
            - "-c"
            - "maintenance_work_mem=64MB"
            - "-c"
            - "checkpoint_completion_target=0.9"
            - "-c"
            - "wal_buffers=16MB"
            - "-c"
            - "default_statistics_target=100"
          
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
          
          resources:
            requests:
              memory: "1Gi"
              cpu: "500m"
            limits:
              memory: "4Gi"
              cpu: "2000m"
          
          livenessProbe:
            exec:
              command:
                - /bin/sh
                - -c
                - pg_isready -U elsa
            initialDelaySeconds: 30
            periodSeconds: 10
          
          readinessProbe:
            exec:
              command:
                - /bin/sh
                - -c
                - pg_isready -U elsa
            initialDelaySeconds: 5
            periodSeconds: 5
  
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: "standard"  # Use your storage class
        resources:
          requests:
            storage: 50Gi
```

### PostgreSQL Service

```yaml
# postgresql/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: elsa-postgresql
  namespace: elsa-workflows
spec:
  type: ClusterIP
  clusterIP: None  # Headless service for StatefulSet
  ports:
    - port: 5432
      targetPort: postgres
      protocol: TCP
      name: postgres
  selector:
    app: postgresql
```

### Database Backup Configuration

Create a CronJob for regular backups:

```yaml
# postgresql/backup-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: postgresql-backup
  namespace: elsa-workflows
spec:
  schedule: "0 2 * * *"  # Daily at 2 AM
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: backup
              image: postgres:16-alpine
              command:
                - /bin/sh
                - -c
                - |
                  TIMESTAMP=$(date +%Y%m%d_%H%M%S)
                  pg_dump -h elsa-postgresql -U elsa -d elsa > /backup/elsa_backup_${TIMESTAMP}.sql
                  # Upload to S3 or other storage
                  # aws s3 cp /backup/elsa_backup_${TIMESTAMP}.sql s3://your-bucket/backups/
              env:
                - name: PGPASSWORD
                  valueFrom:
                    secretKeyRef:
                      name: elsa-secrets
                      key: postgres-password
              volumeMounts:
                - name: backup
                  mountPath: /backup
          restartPolicy: OnFailure
          volumes:
            - name: backup
              persistentVolumeClaim:
                claimName: backup-pvc
```

### Connection Pooling

For high-load scenarios, consider using PgBouncer:

```yaml
# postgresql/pgbouncer-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pgbouncer
  namespace: elsa-workflows
spec:
  replicas: 2
  selector:
    matchLabels:
      app: pgbouncer
  template:
    metadata:
      labels:
        app: pgbouncer
    spec:
      containers:
        - name: pgbouncer
          image: edoburu/pgbouncer:latest
          ports:
            - containerPort: 5432
          env:
            - name: DATABASE_URL
              value: "postgres://elsa:PASSWORD@elsa-postgresql:5432/elsa"
            - name: POOL_MODE
              value: "transaction"
            - name: MAX_CLIENT_CONN
              value: "1000"
            - name: DEFAULT_POOL_SIZE
              value: "25"
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "500m"
```

## Persistent Storage

Proper storage configuration ensures data persistence across pod restarts and upgrades.

### Storage Classes

Define storage classes for different performance tiers:

```yaml
# storage-classes.yaml
---
# Standard storage for general use
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard-retain
provisioner: kubernetes.io/aws-ebs  # or azure-disk, gce-pd
parameters:
  type: gp3
  fsType: ext4
reclaimPolicy: Retain  # Prevent accidental data loss
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer

---
# High-performance storage for databases
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: kubernetes.io/aws-ebs
parameters:
  type: io2
  iopsPerGB: "50"
  fsType: ext4
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
```

### Persistent Volume Claims

```yaml
# pvc.yaml
---
# Database PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgresql-data-pvc
  namespace: elsa-workflows
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: fast-ssd
  resources:
    requests:
      storage: 100Gi

---
# Backup PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: backup-pvc
  namespace: elsa-workflows
spec:
  accessModes:
    - ReadWriteMany  # For multiple backup pods
  storageClassName: standard-retain
  resources:
    requests:
      storage: 500Gi

---
# Redis PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: redis-data-pvc
  namespace: elsa-workflows
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: fast-ssd
  resources:
    requests:
      storage: 20Gi
```

### Volume Snapshots

Configure VolumeSnapshotClass for backup and disaster recovery:

```yaml
# volume-snapshot-class.yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
  name: elsa-snapshot-class
driver: ebs.csi.aws.com  # or disk.csi.azure.com, pd.csi.storage.gke.io
deletionPolicy: Retain
parameters:
  tagSpecification_1: "Name=elsa-workflow-snapshot"
```

Create snapshots:

```yaml
# create-snapshot.yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: postgresql-snapshot
  namespace: elsa-workflows
spec:
  volumeSnapshotClassName: elsa-snapshot-class
  source:
    persistentVolumeClaimName: postgresql-data-pvc
```

## Ingress Setup

Ingress controllers provide external access to your Elsa Workflows deployment with SSL/TLS termination, routing, and load balancing.

### NGINX Ingress Controller

#### Installation

```bash
# Install NGINX Ingress Controller
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

helm install nginx-ingress ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --set controller.service.type=LoadBalancer
```

#### Ingress Configuration

```yaml
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: elsa-ingress
  namespace: elsa-workflows
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
    nginx.ingress.kubernetes.io/rate-limit: "100"
    nginx.ingress.kubernetes.io/limit-rps: "10"
    # CORS configuration
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/cors-allow-origin: "https://studio.example.com"
    nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, PUT, DELETE, OPTIONS"
    nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - studio.example.com
        - api.example.com
      secretName: elsa-tls
  rules:
    - host: studio.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: elsa-studio
                port:
                  number: 80
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: elsa-server
                port:
                  number: 80
```

### Traefik Ingress Controller

#### Installation

```bash
helm repo add traefik https://traefik.github.io/charts
helm repo update

helm install traefik traefik/traefik \
  --namespace traefik \
  --create-namespace \
  --set ports.web.redirectTo.port=websecure
```

#### IngressRoute Configuration

```yaml
# traefik-ingressroute.yaml
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: elsa-studio
  namespace: elsa-workflows
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`studio.example.com`)
      kind: Rule
      services:
        - name: elsa-studio
          port: 80
      middlewares:
        - name: security-headers
  tls:
    secretName: elsa-tls

---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: elsa-server
  namespace: elsa-workflows
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`api.example.com`)
      kind: Rule
      services:
        - name: elsa-server
          port: 80
      middlewares:
        - name: rate-limit
        - name: security-headers
  tls:
    secretName: elsa-tls

---
# Security headers middleware
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: security-headers
  namespace: elsa-workflows
spec:
  headers:
    stsSeconds: 31536000
    stsIncludeSubdomains: true
    stsPreload: true
    forceSTSHeader: true
    contentSecurityPolicy: "default-src 'self'"
    customResponseHeaders:
      X-Frame-Options: "SAMEORIGIN"
      X-Content-Type-Options: "nosniff"

---
# Rate limiting middleware
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: rate-limit
  namespace: elsa-workflows
spec:
  rateLimit:
    average: 100
    burst: 50
```

### SSL/TLS with cert-manager

#### Install cert-manager

```bash
helm repo add jetstack https://charts.jetstack.io
helm repo update

helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --set installCRDs=true
```

#### ClusterIssuer Configuration

```yaml
# cert-manager-issuer.yaml
---
# Let's Encrypt Staging (for testing)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: admin@example.com
    privateKeySecretRef:
      name: letsencrypt-staging
    solvers:
      - http01:
          ingress:
            class: nginx

---
# Let's Encrypt Production
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: admin@example.com
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
      - http01:
          ingress:
            class: nginx
```

## Horizontal Pod Autoscaling

HPA automatically scales pods based on CPU, memory, or custom metrics.

### Metrics Server Installation

```bash
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
```

### HPA for Elsa Server

```yaml
# elsa-server/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: elsa-server-hpa
  namespace: elsa-workflows
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: elsa-server
  minReplicas: 3
  maxReplicas: 10
  metrics:
    # CPU-based scaling
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    
    # Memory-based scaling
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
    
    # Custom metric: requests per second
    - type: Pods
      pods:
        metric:
          name: http_requests_per_second
        target:
          type: AverageValue
          averageValue: "1000"
  
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 50
          periodSeconds: 60
        - type: Pods
          value: 2
          periodSeconds: 60
      selectPolicy: Min
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
        - type: Percent
          value: 100
          periodSeconds: 30
        - type: Pods
          value: 4
          periodSeconds: 30
      selectPolicy: Max
```

### HPA for Elsa Studio

```yaml
# elsa-studio/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: elsa-studio-hpa
  namespace: elsa-workflows
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: elsa-studio
  minReplicas: 2
  maxReplicas: 5
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 75
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
```

### Vertical Pod Autoscaling (Optional)

For automatic resource request adjustments:

```yaml
# elsa-server/vpa.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: elsa-server-vpa
  namespace: elsa-workflows
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: elsa-server
  updatePolicy:
    updateMode: "Auto"  # or "Initial", "Recreate", "Off"
  resourcePolicy:
    containerPolicies:
      - containerName: elsa-server
        minAllowed:
          cpu: 500m
          memory: 512Mi
        maxAllowed:
          cpu: 4000m
          memory: 8Gi
```

### Pod Disruption Budget

Ensure availability during voluntary disruptions:

```yaml
# elsa-server/pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: elsa-server-pdb
  namespace: elsa-workflows
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: elsa-server
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: elsa-studio-pdb
  namespace: elsa-workflows
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: elsa-studio
```

### Testing Autoscaling

```bash
# Watch HPA status
kubectl get hpa -n elsa-workflows --watch

# Generate load to test scaling
kubectl run -i --tty load-generator --rm --image=busybox --restart=Never -- /bin/sh
# Inside the pod:
while true; do wget -q -O- http://elsa-server.elsa-workflows.svc.cluster.local; done

# Monitor pod scaling
kubectl get pods -n elsa-workflows --watch
```

## Monitoring with Prometheus & Grafana

Comprehensive monitoring is essential for production Kubernetes deployments. This section covers Prometheus metrics collection and Grafana dashboards.

### Install Prometheus Stack

```bash
# Add Prometheus Helm repository
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

# Install kube-prometheus-stack (includes Prometheus, Grafana, and Alertmanager)
helm install prometheus prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --create-namespace \
  --set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false \
  --set grafana.adminPassword=admin
```

### ServiceMonitor for Elsa Server

```yaml
# monitoring/elsa-server-servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: elsa-server
  namespace: elsa-workflows
  labels:
    app: elsa-server
    release: prometheus
spec:
  selector:
    matchLabels:
      app: elsa-server
  endpoints:
    - port: http
      path: /metrics
      interval: 30s
      scrapeTimeout: 10s
```

### PrometheusRule for Alerts

```yaml
# monitoring/elsa-alerts.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: elsa-alerts
  namespace: elsa-workflows
  labels:
    release: prometheus
spec:
  groups:
    - name: elsa-workflows
      interval: 30s
      rules:
        # High CPU usage alert
        - alert: ElsaServerHighCPU
          expr: |
            rate(container_cpu_usage_seconds_total{namespace="elsa-workflows",pod=~"elsa-server-.*"}[5m]) > 0.8
          for: 5m
          labels:
            severity: warning
          annotations:
            summary: "Elsa Server high CPU usage"
            description: "Pod {{ $labels.pod }} CPU usage is above 80% for 5 minutes"
        
        # High memory usage alert
        - alert: ElsaServerHighMemory
          expr: |
            container_memory_working_set_bytes{namespace="elsa-workflows",pod=~"elsa-server-.*"} / 
            container_spec_memory_limit_bytes{namespace="elsa-workflows",pod=~"elsa-server-.*"} > 0.9
          for: 5m
          labels:
            severity: warning
          annotations:
            summary: "Elsa Server high memory usage"
            description: "Pod {{ $labels.pod }} memory usage is above 90%"
        
        # Pod restart alert
        - alert: ElsaServerPodRestarting
          expr: |
            rate(kube_pod_container_status_restarts_total{namespace="elsa-workflows",pod=~"elsa-server-.*"}[15m]) > 0
          for: 5m
          labels:
            severity: critical
          annotations:
            summary: "Elsa Server pod restarting"
            description: "Pod {{ $labels.pod }} has restarted {{ $value }} times in the last 15 minutes"
        
        # Low replica count
        - alert: ElsaServerLowReplicas
          expr: |
            kube_deployment_status_replicas_available{namespace="elsa-workflows",deployment="elsa-server"} < 2
          for: 5m
          labels:
            severity: critical
          annotations:
            summary: "Elsa Server low replica count"
            description: "Only {{ $value }} replicas available for elsa-server deployment"
        
        # Database connection errors
        - alert: ElsaDatabaseConnectionErrors
          expr: |
            rate(elsa_database_connection_errors_total[5m]) > 0.1
          for: 2m
          labels:
            severity: critical
          annotations:
            summary: "Elsa database connection errors"
            description: "Database connection error rate is {{ $value }} per second"
        
        # Workflow execution failures
        - alert: ElsaWorkflowExecutionFailures
          expr: |
            rate(elsa_workflow_execution_failed_total[5m]) > 0.5
          for: 5m
          labels:
            severity: warning
          annotations:
            summary: "High workflow execution failure rate"
            description: "Workflow execution failure rate is {{ $value }} per second"
        
        # High response time
        - alert: ElsaServerHighResponseTime
          expr: |
            histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{namespace="elsa-workflows"}[5m])) > 2
          for: 5m
          labels:
            severity: warning
          annotations:
            summary: "Elsa Server high response time"
            description: "95th percentile response time is {{ $value }} seconds"
```

### Grafana Dashboard

Create a comprehensive Grafana dashboard for Elsa Workflows:

```yaml
# monitoring/elsa-dashboard-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: elsa-grafana-dashboard
  namespace: monitoring
  labels:
    grafana_dashboard: "1"
data:
  elsa-workflows.json: |
    {
      "dashboard": {
        "title": "Elsa Workflows",
        "timezone": "browser",
        "schemaVersion": 16,
        "refresh": "30s",
        "panels": [
          {
            "title": "Request Rate",
            "targets": [
              {
                "expr": "rate(http_requests_total{namespace=\"elsa-workflows\"}[5m])",
                "legendFormat": "{{pod}}"
              }
            ],
            "type": "graph"
          },
          {
            "title": "Response Time (95th Percentile)",
            "targets": [
              {
                "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{namespace=\"elsa-workflows\"}[5m]))",
                "legendFormat": "{{pod}}"
              }
            ],
            "type": "graph"
          },
          {
            "title": "CPU Usage",
            "targets": [
              {
                "expr": "rate(container_cpu_usage_seconds_total{namespace=\"elsa-workflows\"}[5m])",
                "legendFormat": "{{pod}}"
              }
            ],
            "type": "graph"
          },
          {
            "title": "Memory Usage",
            "targets": [
              {
                "expr": "container_memory_working_set_bytes{namespace=\"elsa-workflows\"} / 1024 / 1024",
                "legendFormat": "{{pod}}"
              }
            ],
            "type": "graph"
          },
          {
            "title": "Active Workflows",
            "targets": [
              {
                "expr": "elsa_active_workflows_total{namespace=\"elsa-workflows\"}",
                "legendFormat": "{{pod}}"
              }
            ],
            "type": "stat"
          },
          {
            "title": "Workflow Execution Rate",
            "targets": [
              {
                "expr": "rate(elsa_workflow_executions_total{namespace=\"elsa-workflows\"}[5m])",
                "legendFormat": "{{status}}"
              }
            ],
            "type": "graph"
          },
          {
            "title": "Database Connection Pool",
            "targets": [
              {
                "expr": "elsa_database_connections_active{namespace=\"elsa-workflows\"}",
                "legendFormat": "Active"
              },
              {
                "expr": "elsa_database_connections_idle{namespace=\"elsa-workflows\"}",
                "legendFormat": "Idle"
              }
            ],
            "type": "graph"
          },
          {
            "title": "Pod Status",
            "targets": [
              {
                "expr": "kube_pod_status_phase{namespace=\"elsa-workflows\"}",
                "legendFormat": "{{pod}} - {{phase}}"
              }
            ],
            "type": "table"
          }
        ]
      }
    }
```

### Custom Metrics in Elsa

To expose custom metrics from your Elsa Server, configure Prometheus metrics in `Program.cs`:

```csharp
using Prometheus;

var builder = WebApplication.CreateBuilder(args);

// Configure Elsa
builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowManagement();
    elsa.UseWorkflowRuntime();
    elsa.UseWorkflowsApi();
});

var app = builder.Build();

// Enable Prometheus metrics endpoint
app.UseMetricServer();  // Exposes /metrics endpoint
app.UseHttpMetrics();   // Collect HTTP metrics

app.UseWorkflowsApi();
app.Run();
```

### Accessing Grafana

```bash
# Port-forward Grafana service
kubectl port-forward -n monitoring svc/prometheus-grafana 3000:80

# Access Grafana at http://localhost:3000
# Default credentials: admin / admin (or password set during installation)
```

### Key Metrics to Monitor

| Metric                                     | Description                | Alert Threshold |
| ------------------------------------------ | -------------------------- | --------------- |
| `http_requests_total`                      | Total HTTP requests        | -               |
| `http_request_duration_seconds`            | Request latency            | P95 > 2s        |
| `elsa_workflow_executions_total`           | Workflow executions        | -               |
| `elsa_workflow_execution_failed_total`     | Failed workflows           | Rate > 0.5/s    |
| `elsa_active_workflows_total`              | Currently active workflows | -               |
| `elsa_database_connections_active`         | Active DB connections      | > 90% of pool   |
| `container_cpu_usage_seconds_total`        | CPU usage                  | > 80%           |
| `container_memory_working_set_bytes`       | Memory usage               | > 90% of limit  |
| `kube_pod_container_status_restarts_total` | Pod restarts               | > 0 in 15min    |

## Service Mesh Integration

Service meshes provide advanced traffic management, security, and observability features. This section covers integration with Istio and Linkerd.

### Istio Integration

#### Prerequisites

```bash
# Download and install Istio
curl -L https://istio.io/downloadIstio | sh -
cd istio-*
export PATH=$PWD/bin:$PATH

# Install Istio with demo profile
istioctl install --set profile=demo -y

# Enable sidecar injection for elsa-workflows namespace
kubectl label namespace elsa-workflows istio-injection=enabled
```

#### Gateway Configuration

```yaml
# istio/gateway.yaml
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  name: elsa-gateway
  namespace: elsa-workflows
spec:
  selector:
    istio: ingressgateway
  servers:
    - port:
        number: 443
        name: https
        protocol: HTTPS
      tls:
        mode: SIMPLE
        credentialName: elsa-tls
      hosts:
        - studio.example.com
        - api.example.com
    - port:
        number: 80
        name: http
        protocol: HTTP
      hosts:
        - studio.example.com
        - api.example.com
      tls:
        httpsRedirect: true
```

#### VirtualService Configuration

```yaml
# istio/virtualservice.yaml
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: elsa-studio
  namespace: elsa-workflows
spec:
  hosts:
    - studio.example.com
  gateways:
    - elsa-gateway
  http:
    - match:
        - uri:
            prefix: /
      route:
        - destination:
            host: elsa-studio
            port:
              number: 80
          weight: 100
      timeout: 30s
      retries:
        attempts: 3
        perTryTimeout: 10s
        retryOn: 5xx,reset,connect-failure,refused-stream

---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: elsa-server
  namespace: elsa-workflows
spec:
  hosts:
    - api.example.com
  gateways:
    - elsa-gateway
  http:
    - match:
        - uri:
            prefix: /
      route:
        - destination:
            host: elsa-server
            port:
              number: 80
          weight: 100
      timeout: 60s
      retries:
        attempts: 3
        perTryTimeout: 20s
        retryOn: 5xx,reset,connect-failure,refused-stream
```

#### DestinationRule for Circuit Breaking

```yaml
# istio/destinationrule.yaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: elsa-server
  namespace: elsa-workflows
spec:
  host: elsa-server
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 1000
      http:
        http1MaxPendingRequests: 1000
        http2MaxRequests: 1000
        maxRequestsPerConnection: 2
    loadBalancer:
      simple: LEAST_REQUEST
    outlierDetection:
      consecutiveErrors: 5
      interval: 30s
      baseEjectionTime: 30s
      maxEjectionPercent: 50
      minHealthPercent: 40
```

#### PeerAuthentication for mTLS

```yaml
# istio/peerauthentication.yaml
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: elsa-workflows
spec:
  mtls:
    mode: STRICT
```

#### AuthorizationPolicy

```yaml
# istio/authorizationpolicy.yaml
---
# Allow traffic from ingress to services
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: allow-ingress
  namespace: elsa-workflows
spec:
  selector:
    matchLabels:
      app: elsa-server
  action: ALLOW
  rules:
    - from:
        - source:
            namespaces: ["istio-system"]

---
# Deny all by default, then allow specific paths
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: elsa-server-authz
  namespace: elsa-workflows
spec:
  selector:
    matchLabels:
      app: elsa-server
  action: ALLOW
  rules:
    - to:
        - operation:
            methods: ["GET", "POST", "PUT", "DELETE"]
            paths: ["/elsa/api/*", "/health/*", "/metrics"]
```

### Linkerd Integration

#### Installation

```bash
# Install Linkerd CLI
curl -sL https://run.linkerd.io/install | sh
export PATH=$PATH:$HOME/.linkerd2/bin

# Verify cluster compatibility
linkerd check --pre

# Install Linkerd control plane
linkerd install | kubectl apply -f -

# Verify installation
linkerd check

# Install Linkerd Viz for observability
linkerd viz install | kubectl apply -f -
```

#### Mesh Elsa Workflows Namespace

```bash
# Inject Linkerd sidecar into existing deployments
kubectl get deploy -n elsa-workflows -o yaml | \
  linkerd inject - | \
  kubectl apply -f -

# Or annotate namespace for automatic injection
kubectl annotate namespace elsa-workflows linkerd.io/inject=enabled
```

#### Traffic Split for Canary Deployments

```yaml
# linkerd/trafficsplit.yaml
apiVersion: split.smi-spec.io/v1alpha1
kind: TrafficSplit
metadata:
  name: elsa-server-split
  namespace: elsa-workflows
spec:
  service: elsa-server
  backends:
    - service: elsa-server-stable
      weight: 90
    - service: elsa-server-canary
      weight: 10
```

#### ServiceProfile for Advanced Metrics

```yaml
# linkerd/serviceprofile.yaml
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
  name: elsa-server.elsa-workflows.svc.cluster.local
  namespace: elsa-workflows
spec:
  routes:
    - name: POST /elsa/api/workflows
      condition:
        method: POST
        pathRegex: /elsa/api/workflows
      timeout: 30s
      retries:
        limit: 3
        timeout: 10s
    
    - name: GET /elsa/api/workflows
      condition:
        method: GET
        pathRegex: /elsa/api/workflows.*
      timeout: 10s
    
    - name: Health Check
      condition:
        pathRegex: /health.*
      isRetryable: true
```

#### Rate Limiting with Linkerd

```yaml
# linkerd/ratelimit.yaml
apiVersion: policy.linkerd.io/v1alpha1
kind: HTTPRoute
metadata:
  name: elsa-server-ratelimit
  namespace: elsa-workflows
spec:
  parentRefs:
    - name: elsa-server
      kind: Service
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /elsa/api
      filters:
        - type: RequestHeaderModifier
          requestHeaderModifier:
            add:
              - name: X-RateLimit-Limit
                value: "100"
```

### Observability with Service Mesh

#### Istio Dashboard

```bash
# Access Kiali dashboard
istioctl dashboard kiali

# Access Jaeger for distributed tracing
istioctl dashboard jaeger

# Access Prometheus
istioctl dashboard prometheus

# Access Grafana
istioctl dashboard grafana
```

#### Linkerd Dashboard

```bash
# Access Linkerd dashboard
linkerd viz dashboard

# View traffic metrics
linkerd viz stat deploy -n elsa-workflows

# View route metrics
linkerd viz routes deploy/elsa-server -n elsa-workflows

# Tap live traffic (for debugging)
linkerd viz tap deploy/elsa-server -n elsa-workflows
```

### Comparison: Istio vs Linkerd

| Feature                | Istio                     | Linkerd                  |
| ---------------------- | ------------------------- | ------------------------ |
| **Learning Curve**     | Steep                     | Gentle                   |
| **Resource Usage**     | Higher (Envoy proxy)      | Lower (Linkerd2-proxy)   |
| **Features**           | Comprehensive             | Focused                  |
| **Traffic Management** | Advanced                  | Basic                    |
| **Security**           | mTLS, AuthZ policies      | mTLS, policy             |
| **Observability**      | Prometheus, Jaeger, Kiali | Prometheus, built-in viz |
| **Performance**        | Good                      | Excellent                |
| **Best For**           | Complex environments      | Simplicity, performance  |

### Service Mesh Best Practices

1. **Start Simple**: Begin without a service mesh and add it when needed
2. **Resource Planning**: Allocate extra resources for sidecar proxies (\~50-100Mi RAM, 0.1 CPU per pod)
3. **Gradual Rollout**: Enable mesh incrementally, namespace by namespace
4. **Monitor Performance**: Watch for latency increases due to proxy overhead
5. **Use mTLS**: Enable mutual TLS for secure pod-to-pod communication
6. **Circuit Breaking**: Configure circuit breakers to prevent cascade failures
7. **Observability**: Leverage built-in tracing and metrics
8. **Test Thoroughly**: Test failure scenarios with chaos engineering

## Distributed Configuration

For Kubernetes deployments with multiple replicas, proper distributed configuration is essential. Reference the [Distributed Hosting](/hosting/distributed-hosting) guide for detailed configuration.

### Distributed Runtime Configuration

Configure distributed workflow runtime in your Elsa Server:

```csharp
// Program.cs or Startup.cs
using Elsa.Extensions;
using Elsa.DistributedLocking.Extensions;
using Medallion.Threading.Postgres;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddElsa(elsa =>
{
    // Configure distributed workflow runtime
    elsa.UseWorkflowRuntime(runtime =>
    {
        runtime.UseDistributedRuntime();
        
        // Configure distributed locking with PostgreSQL
        runtime.DistributedLockProvider = serviceProvider => 
            new PostgresDistributedSynchronizationProvider(
                builder.Configuration.GetConnectionString("PostgreSql"),
                options =>
                {
                    options.KeepaliveCadence(TimeSpan.FromMinutes(5));
                    options.UseMultiplexing();
                });
    });
    
    // Configure distributed caching with MassTransit
    elsa.UseDistributedCache(distributedCaching =>
    {
        distributedCaching.UseMassTransit();
    });
    
    // Configure MassTransit with RabbitMQ
    elsa.UseMassTransit(massTransit =>
    {
        massTransit.UseRabbitMq(
            builder.Configuration.GetConnectionString("RabbitMq"),
            rabbit =>
            {
                rabbit.ConfigureTransportBus = (context, bus) =>
                {
                    bus.PrefetchCount = 50;
                    bus.Durable = true;
                    bus.AutoDelete = false;
                    bus.ConcurrentMessageLimit = 32;
                };
            });
    });
    
    // Configure Quartz.NET with PostgreSQL for distributed scheduling
    elsa.UseScheduling(scheduling =>
    {
        scheduling.UseQuartzScheduler();
    });
});

// Configure Quartz with persistent store
builder.Services.AddQuartz(quartz =>
{
    quartz.UsePostgreSql(builder.Configuration.GetConnectionString("PostgreSql"));
});

var app = builder.Build();
app.Run();
```

### Environment-Based Configuration

Use Kubernetes ConfigMaps and Secrets for environment-specific settings:

```yaml
# distributed-config-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: elsa-distributed-config
  namespace: elsa-workflows
data:
  # Elsa Configuration
  ELSA__RUNTIME__TYPE: "Distributed"
  ELSA__CACHING__TYPE: "Distributed"
  ELSA__LOCKING__PROVIDER: "PostgreSQL"
  
  # MassTransit Configuration
  MASSTRANSIT__TRANSPORT: "RabbitMq"
  MASSTRANSIT__PREFETCHCOUNT: "50"
  
  # Quartz Configuration
  QUARTZ__CLUSTERED: "true"
  QUARTZ__INSTANCENAME: "ElsaQuartzCluster"
  
  # Performance Tuning
  ASPNETCORE__KESTREL__LIMITS__MAXCONCURRENTCONNECTIONS: "1000"
  ASPNETCORE__KESTREL__LIMITS__MAXREQUESTBODYSIZE: "10485760"
```

Apply to deployment:

```yaml
# Add to elsa-server deployment
spec:
  template:
    spec:
      containers:
        - name: elsa-server
          envFrom:
            - configMapRef:
                name: elsa-distributed-config
            - secretRef:
                name: elsa-secrets
```

### Redis Configuration for Caching

Deploy Redis for distributed caching:

```yaml
# redis/statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: elsa-redis
  namespace: elsa-workflows
spec:
  serviceName: elsa-redis
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
        - name: redis
          image: redis:7-alpine
          command:
            - redis-server
            - --appendonly
            - "yes"
            - --maxmemory
            - "1gb"
            - --maxmemory-policy
            - "allkeys-lru"
          ports:
            - containerPort: 6379
              name: redis
          volumeMounts:
            - name: data
              mountPath: /data
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "1Gi"
              cpu: "1000m"
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 10Gi
```

## Troubleshooting

Common issues and their solutions when deploying Elsa to Kubernetes.

### Pod Issues

#### Pods Not Starting

**Symptom**: Pods stuck in `Pending` or `ImagePullBackOff` state

```bash
# Check pod status
kubectl describe pod <pod-name> -n elsa-workflows

# Common issues and solutions:
```

**Solution 1: Insufficient Resources**

```bash
# Check node resources
kubectl top nodes

# Check resource requests
kubectl describe node <node-name>

# Solution: Scale cluster or reduce resource requests
```

**Solution 2: Image Pull Issues**

```bash
# Check image pull secrets
kubectl get secrets -n elsa-workflows

# Create image pull secret if needed
kubectl create secret docker-registry regcred \
  --docker-server=<registry-server> \
  --docker-username=<username> \
  --docker-password=<password> \
  --docker-email=<email> \
  -n elsa-workflows

# Add to deployment
spec:
  template:
    spec:
      imagePullSecrets:
        - name: regcred
```

#### Pods Crashing (CrashLoopBackOff)

**Symptom**: Pods repeatedly restarting

```bash
# View logs
kubectl logs <pod-name> -n elsa-workflows --previous

# Check events
kubectl get events -n elsa-workflows --sort-by='.lastTimestamp'
```

**Common Causes:**

1. **Database Connection Issues**

```bash
# Test database connectivity
kubectl run -it --rm debug --image=postgres:16-alpine --restart=Never -- \
  psql -h elsa-postgresql -U elsa -d elsa

# Check connection string in secrets
kubectl get secret elsa-secrets -n elsa-workflows -o jsonpath='{.data.postgresql-connection-string}' | base64 -d
```

2. **Missing Dependencies**

```bash
# Check if Redis/RabbitMQ are running
kubectl get pods -n elsa-workflows

# Check service endpoints
kubectl get endpoints -n elsa-workflows
```

3. **Configuration Errors**

```bash
# Validate ConfigMaps and Secrets
kubectl get configmap elsa-config -n elsa-workflows -o yaml
kubectl get secret elsa-secrets -n elsa-workflows -o yaml
```

### Database Issues

#### Migration Failures

**Symptom**: Elsa Server fails to start due to database migration errors

```bash
# Run migrations manually using a Job
kubectl apply -f - <<EOF
apiVersion: batch/v1
kind: Job
metadata:
  name: elsa-db-migration
  namespace: elsa-workflows
spec:
  template:
    spec:
      containers:
        - name: migration
          image: elsaworkflows/elsa-server-v3:latest
          command: ["/bin/sh", "-c"]
          args:
            - |
              dotnet ef database update
          env:
            - name: CONNECTIONSTRINGS__POSTGRESQL
              valueFrom:
                secretKeyRef:
                  name: elsa-secrets
                  key: postgresql-connection-string
      restartPolicy: Never
  backoffLimit: 3
EOF

# Check job logs
kubectl logs job/elsa-db-migration -n elsa-workflows
```

#### Connection Pool Exhaustion

**Symptom**: "Timeout expired" or "Too many connections" errors

```bash
# Check current connections
kubectl exec -it elsa-postgresql-0 -n elsa-workflows -- \
  psql -U elsa -d elsa -c "SELECT count(*) FROM pg_stat_activity;"

# Solution: Increase max_connections or connection pool size
# Update PostgreSQL configuration
kubectl edit statefulset elsa-postgresql -n elsa-workflows

# Or use PgBouncer (see Database Configuration section)
```

### Network Issues

#### Service Not Accessible

**Symptom**: Cannot reach Elsa services from outside cluster

```bash
# Check service
kubectl get svc -n elsa-workflows

# Check endpoints
kubectl get endpoints elsa-server -n elsa-workflows

# Check ingress
kubectl get ingress -n elsa-workflows
kubectl describe ingress elsa-ingress -n elsa-workflows

# Test internal connectivity
kubectl run -it --rm debug --image=curlimages/curl --restart=Never -- \
  curl http://elsa-server.elsa-workflows.svc.cluster.local/health
```

**Solution: DNS Issues**

```bash
# Test DNS resolution
kubectl run -it --rm debug --image=busybox --restart=Never -- \
  nslookup elsa-server.elsa-workflows.svc.cluster.local

# Check CoreDNS
kubectl get pods -n kube-system -l k8s-app=kube-dns
kubectl logs -n kube-system -l k8s-app=kube-dns
```

#### Ingress Not Working

```bash
# Check ingress controller
kubectl get pods -n ingress-nginx

# Check ingress class
kubectl get ingressclass

# Verify TLS certificate
kubectl get certificate -n elsa-workflows
kubectl describe certificate elsa-tls -n elsa-workflows

# Check cert-manager logs if using Let's Encrypt
kubectl logs -n cert-manager deployment/cert-manager
```

### Performance Issues

#### High Latency

```bash
# Check pod metrics
kubectl top pods -n elsa-workflows

# Check HPA status
kubectl get hpa -n elsa-workflows

# View detailed metrics
kubectl describe hpa elsa-server-hpa -n elsa-workflows
```

**Solutions:**

* Increase replica count
* Optimize database queries
* Add caching layer
* Review resource limits

#### Memory Leaks

```bash
# Monitor memory usage over time
kubectl top pod <pod-name> -n elsa-workflows --containers

# Get heap dump (if .NET diagnostics enabled)
kubectl exec -it <pod-name> -n elsa-workflows -- \
  dotnet-dump collect --process-id 1
```

### Distributed Configuration Issues

#### Lock Acquisition Failures

**Symptom**: "Failed to acquire lock" errors in logs

```bash
# Check distributed lock table in database
kubectl exec -it elsa-postgresql-0 -n elsa-workflows -- \
  psql -U elsa -d elsa -c "SELECT * FROM distributed_locks;"

# Clear stale locks (use with caution)
kubectl exec -it elsa-postgresql-0 -n elsa-workflows -- \
  psql -U elsa -d elsa -c "DELETE FROM distributed_locks WHERE acquired_at < NOW() - INTERVAL '1 hour';"
```

#### Cache Invalidation Issues

**Symptom**: Stale data across pods

```bash
# Check RabbitMQ queues
kubectl exec -it elsa-rabbitmq-0 -n elsa-workflows -- rabbitmqctl list_queues

# Verify MassTransit configuration
kubectl logs <elsa-server-pod> -n elsa-workflows | grep -i masstransit

# Restart all pods to force cache refresh
kubectl rollout restart deployment/elsa-server -n elsa-workflows
```

### Debugging Commands

```bash
# Get all resources in namespace
kubectl get all -n elsa-workflows

# Describe all pods
kubectl describe pods -n elsa-workflows

# View logs from all pods
kubectl logs -n elsa-workflows -l app=elsa-server --tail=100

# Follow logs in real-time
kubectl logs -f <pod-name> -n elsa-workflows

# Execute commands in pod
kubectl exec -it <pod-name> -n elsa-workflows -- /bin/sh

# Port-forward for local access
kubectl port-forward svc/elsa-server 8080:80 -n elsa-workflows

# Get resource usage
kubectl top pods -n elsa-workflows
kubectl top nodes

# Check cluster events
kubectl get events -n elsa-workflows --sort-by='.lastTimestamp'

# Validate YAML before applying
kubectl apply --dry-run=client -f deployment.yaml

# Explain resource fields
kubectl explain deployment.spec.template.spec.containers
```

## Production Best Practices

Follow these best practices for reliable, secure, and performant Kubernetes deployments.

### Security

#### 1. Use Non-Root Containers

```yaml
securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  fsGroup: 1000
  capabilities:
    drop:
      - ALL
  readOnlyRootFilesystem: false  # Set to true if possible
```

#### 2. Network Policies

Restrict pod-to-pod communication:

```yaml
# network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: elsa-server-netpol
  namespace: elsa-workflows
spec:
  podSelector:
    matchLabels:
      app: elsa-server
  policyTypes:
    - Ingress
    - Egress
  ingress:
    # Allow from ingress controller
    - from:
        - namespaceSelector:
            matchLabels:
              name: ingress-nginx
      ports:
        - protocol: TCP
          port: 8080
    # Allow from Studio
    - from:
        - podSelector:
            matchLabels:
              app: elsa-studio
      ports:
        - protocol: TCP
          port: 8080
  egress:
    # Allow to database
    - to:
        - podSelector:
            matchLabels:
              app: postgresql
      ports:
        - protocol: TCP
          port: 5432
    # Allow to Redis
    - to:
        - podSelector:
            matchLabels:
              app: redis
      ports:
        - protocol: TCP
          port: 6379
    # Allow to RabbitMQ
    - to:
        - podSelector:
            matchLabels:
              app: rabbitmq
      ports:
        - protocol: TCP
          port: 5672
    # Allow DNS
    - to:
        - namespaceSelector:
            matchLabels:
              name: kube-system
      ports:
        - protocol: UDP
          port: 53
```

#### 3. Secrets Management

Use external secret management:

```yaml
# external-secrets-operator example
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-secrets-manager
  namespace: elsa-workflows
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        jwt:
          serviceAccountRef:
            name: elsa-server

---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: elsa-secrets
  namespace: elsa-workflows
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: SecretStore
  target:
    name: elsa-secrets
    creationPolicy: Owner
  data:
    - secretKey: postgresql-connection-string
      remoteRef:
        key: elsa/production/database
        property: connection-string
```

#### 4. RBAC Configuration

```yaml
# rbac.yaml
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: elsa-server
  namespace: elsa-workflows

---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: elsa-server-role
  namespace: elsa-workflows
rules:
  - apiGroups: [""]
    resources: ["configmaps", "secrets"]
    verbs: ["get", "list", "watch"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: elsa-server-rolebinding
  namespace: elsa-workflows
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: elsa-server-role
subjects:
  - kind: ServiceAccount
    name: elsa-server
    namespace: elsa-workflows
```

### High Availability

#### 1. Multi-Zone Deployment

```yaml
spec:
  template:
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: app
                    operator: In
                    values:
                      - elsa-server
              topologyKey: topology.kubernetes.io/zone
```

#### 2. Pod Disruption Budgets

Ensure minimum availability during disruptions:

```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: elsa-server-pdb
  namespace: elsa-workflows
spec:
  minAvailable: 2  # or maxUnavailable: 1
  selector:
    matchLabels:
      app: elsa-server
```

#### 3. Health Checks

Configure appropriate health checks:

```yaml
livenessProbe:
  httpGet:
    path: /health/live
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 3

readinessProbe:
  httpGet:
    path: /health/ready
    port: 8080
  initialDelaySeconds: 20
  periodSeconds: 5
  timeoutSeconds: 3
  failureThreshold: 3

startupProbe:
  httpGet:
    path: /health/startup
    port: 8080
  initialDelaySeconds: 0
  periodSeconds: 5
  failureThreshold: 30  # Allow up to 150s for startup
```

### Resource Management

#### 1. Set Resource Requests and Limits

```yaml
resources:
  requests:
    memory: "512Mi"
    cpu: "500m"
  limits:
    memory: "2Gi"
    cpu: "2000m"
```

#### 2. Quality of Service Classes

* **Guaranteed**: requests == limits (highest priority)
* **Burstable**: requests < limits (medium priority)
* **BestEffort**: no requests/limits (lowest priority)

#### 3. Limit Ranges

```yaml
# limitrange.yaml
apiVersion: v1
kind: LimitRange
metadata:
  name: elsa-limits
  namespace: elsa-workflows
spec:
  limits:
    - max:
        memory: "4Gi"
        cpu: "4000m"
      min:
        memory: "256Mi"
        cpu: "250m"
      default:
        memory: "1Gi"
        cpu: "1000m"
      defaultRequest:
        memory: "512Mi"
        cpu: "500m"
      type: Container
```

### Backup and Disaster Recovery

#### 1. Regular Backups

```bash
# Backup script example
#!/bin/bash
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

# Database backup
kubectl exec -n elsa-workflows elsa-postgresql-0 -- \
  pg_dump -U elsa elsa | gzip > backup_${TIMESTAMP}.sql.gz

# Upload to S3
aws s3 cp backup_${TIMESTAMP}.sql.gz s3://your-bucket/backups/

# Kubernetes resource backup
kubectl get all,configmap,secret,pvc,ingress -n elsa-workflows -o yaml > \
  k8s_backup_${TIMESTAMP}.yaml
```

#### 2. Velero for Cluster Backups

```bash
# Install Velero
velero install \
  --provider aws \
  --plugins velero/velero-plugin-for-aws:v1.8.0 \
  --bucket velero-backups \
  --backup-location-config region=us-east-1 \
  --snapshot-location-config region=us-east-1

# Create backup schedule
velero schedule create elsa-daily \
  --schedule="0 2 * * *" \
  --include-namespaces elsa-workflows

# List available backups
velero backup get

# Restore from a specific backup
velero restore create --from-backup <backup-name>
```

### Monitoring and Alerting

#### 1. Define SLIs/SLOs

| Service     | SLI                  | SLO      |
| ----------- | -------------------- | -------- |
| Elsa Server | Request Success Rate | > 99.9%  |
| Elsa Server | P95 Latency          | < 500ms  |
| Elsa Server | Availability         | > 99.95% |
| Database    | Connection Success   | > 99.99% |

#### 2. Alert on SLO Violations

```yaml
# prometheus-rules.yaml
- alert: SLOViolation-SuccessRate
  expr: |
    (
      sum(rate(http_requests_total{namespace="elsa-workflows",code=~"2.."}[5m]))
      /
      sum(rate(http_requests_total{namespace="elsa-workflows"}[5m]))
    ) < 0.999
  for: 5m
  labels:
    severity: critical
  annotations:
    summary: "Success rate below SLO (99.9%)"
```

### Cost Optimization

#### 1. Right-Size Resources

```bash
# Use VPA recommendations
kubectl describe vpa elsa-server-vpa -n elsa-workflows

# Monitor actual usage
kubectl top pods -n elsa-workflows --containers
```

#### 2. Use Spot/Preemptible Instances

```yaml
# Node affinity for spot instances
spec:
  template:
    spec:
      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              preference:
                matchExpressions:
                  - key: node.kubernetes.io/lifecycle
                    operator: In
                    values:
                      - spot
```

#### 3. Enable Cluster Autoscaler

```bash
# AWS example
kubectl apply -f https://raw.githubusercontent.com/kubernetes/autoscaler/master/cluster-autoscaler/cloudprovider/aws/examples/cluster-autoscaler-autodiscover.yaml
```

### CI/CD Integration

#### 1. GitOps with ArgoCD

```yaml
# argocd-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: elsa-workflows
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/elsa-k8s
    targetRevision: main
    path: k8s/overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: elsa-workflows
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
```

#### 2. CI Pipeline Example

```yaml
# .github/workflows/deploy.yaml
name: Deploy to Kubernetes
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Configure kubectl
        uses: azure/k8s-set-context@v3
        with:
          method: kubeconfig
          kubeconfig: ${{ secrets.KUBE_CONFIG }}
      
      - name: Deploy
        run: |
          kubectl apply -f k8s/
          kubectl rollout status deployment/elsa-server -n elsa-workflows
```

## Next Steps

After deploying Elsa Workflows to Kubernetes:

1. **Configure Monitoring**: Set up Grafana dashboards and alerts
2. **Test Failure Scenarios**: Use chaos engineering tools like Chaos Mesh
3. **Optimize Performance**: Profile and tune based on your workload
4. **Implement Backups**: Set up automated backup and restore procedures
5. **Security Hardening**: Implement network policies, RBAC, and secret rotation
6. **Documentation**: Document your specific configuration and runbooks

## Related Resources

* [Distributed Hosting Guide](/hosting/distributed-hosting) - Configure distributed runtime
* [Database Configuration](/getting-started/database-configuration) - Database setup details
* [Authentication Guide](/guides/authentication) - Secure your deployment
* [Docker Compose Guide](/getting-started/containers/docker-compose/docker-quickstart) - Local testing
* [Elsa Server Application Type](/application-types/elsa-server) - Server configuration
* [Elsa Studio Application Type](/application-types/elsa-studio) - Studio configuration

## Community and Support

* [Elsa Workflows GitHub](https://github.com/elsa-workflows/elsa-core)
* [GitHub Discussions](https://github.com/elsa-workflows/elsa-core/discussions)
* [GitHub Issues](https://github.com/elsa-workflows/elsa-core/issues)
* Join the community on Discord or Slack

## Version Information

This guide is written for:

* **Elsa Workflows**: v3.7.0
* **Kubernetes**: v1.28+
* **Helm**: v3.12+
* **PostgreSQL**: 16+
* **Redis**: 7+
* **RabbitMQ**: 3.12+

Always refer to the [official releases](https://github.com/elsa-workflows/elsa-core/releases) for the latest version compatibility information.

***

**Last Updated**: 2025-11-20


# Integration

This section provides guides for integrating Elsa Workflows with other technologies and frameworks.

## Guides in This Section

* [Blazor Dashboard](/guides/integration/blazor-dashboard) - Integrating Elsa Studio with Blazor Server applications, covering hosting patterns and authentication configuration.
* [Message Broker Topology](/guides/integration/message-broker-topology) - Choosing endpoint ownership, consumer placement, temporary-resource cleanup, and distributed cache invalidation.

## Related Documentation

* [Elsa Studio](/application-types/elsa-studio) - Studio application overview
* [Hosting Elsa in an Existing App](/guides/onboarding/hosting-elsa-in-existing-app) - General integration guide
* [Security & Hardening](/guides/security) - Deployment security guidance
* [HTTP Workflows](/guides/http-workflows) - Building HTTP-triggered workflows
* [External Application Interaction](/guides/external-application-interaction) - Integrating workflows with external systems


# Blazor Dashboard

Guide to integrating Elsa Studio with Blazor Server applications, covering hosting patterns, authentication configuration, and troubleshooting common issues.

This guide covers integrating Elsa Studio (the workflow designer UI) with Blazor Server applications. You'll learn different hosting patterns, how to configure authentication, and how to troubleshoot common integration issues.

## Overview

Elsa Studio can be integrated with Blazor Server apps in several ways:

* **Same Process**: Host Elsa Server endpoints and Elsa Studio in the same ASP.NET Core application
* **Separate Services**: Host Elsa Server as a separate service and connect Elsa Studio via HTTP
* **Hybrid**: Mix of both approaches for different environments

This guide focuses primarily on Blazor Server integration, which provides a simpler hosting model compared to Blazor WebAssembly.

## Prerequisites

* ASP.NET Core 8.0+ application
* Blazor Server app (or willingness to add Blazor Server to an existing app)
* Basic understanding of Blazor authentication and authorization
* Elsa Server already set up (see [Hosting Elsa in an Existing App](/guides/onboarding/hosting-elsa-in-existing-app))

## Hosting Patterns

### Pattern 1: Single Process (Recommended for Small Teams)

In this pattern, both Elsa Server (workflow runtime + API) and Elsa Studio (UI) run in the same ASP.NET Core process.

**Advantages:**

* Simpler deployment (single service)
* Easier authentication setup (shared auth context)
* Lower latency (no network hop between UI and API)
* Suitable for small to medium workloads

**Disadvantages:**

* UI and runtime share resources (memory, CPU)
* Scaling requires scaling both components together
* UI restarts affect runtime and vice versa

**Architecture:**

```
┌─────────────────────────────────────────────┐
│         Blazor Server Application           │
│                                             │
│  ┌─────────────┐      ┌──────────────┐     │
│  │ Elsa Studio │─────>│ Elsa Server  │     │
│  │  (Blazor)   │ API  │   (Runtime)  │     │
│  └─────────────┘      └──────────────┘     │
│                             │               │
│                             v               │
│                       ┌──────────┐          │
│                       │ Database │          │
│                       └──────────┘          │
└─────────────────────────────────────────────┘
```

### Pattern 2: Separate Services (Recommended for Production)

Elsa Server runs as a standalone service, and Elsa Studio connects to it via HTTP.

**Advantages:**

* Independent scaling (scale runtime without scaling UI)
* Better isolation (UI issues don't affect workflow execution)
* Multiple Studio instances can connect to one Server
* Easier to secure and monitor separately

**Disadvantages:**

* More complex deployment (two services)
* Network latency between UI and API
* Requires proper authentication/authorization setup
* CORS configuration needed

**Architecture:**

```
┌───────────────────┐          ┌───────────────────┐
│  Elsa Studio      │          │   Elsa Server     │
│  (Blazor Server)  │─────────>│   (API Service)   │
│                   │   HTTP   │                   │
└───────────────────┘          └─────────┬─────────┘
                                         │
                                         v
                                   ┌──────────┐
                                   │ Database │
                                   └──────────┘
```

## Implementation: Single Process Pattern

### Step 1: Install Required Packages

```bash
# Add Blazor Server support (if not already present)
dotnet add package Microsoft.AspNetCore.Components.Web

# Add Elsa Server packages
dotnet add package Elsa
dotnet add package Elsa.Workflows.Runtime
dotnet add package Elsa.Workflows.Api
dotnet add package Elsa.Persistence.EFCore.PostgreSql  # or your chosen provider

# Add Elsa Studio packages
dotnet add package Elsa.Studio
dotnet add package Elsa.Studio.Core.BlazorWasm
```

### Step 2: Configure Services in Program.cs

```csharp
using Elsa.Extensions;
using Elsa.Studio.Contracts;
using Elsa.Studio.Core.BlazorServer.Extensions;
using Elsa.Studio.Dashboard.Extensions;
using Elsa.Studio.Extensions;
using Elsa.Studio.Models;
using Elsa.Studio.Shell.Extensions;
using Elsa.Studio.Workflows.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Add Blazor Server
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor(options =>
{
    options.RootComponents.RegisterCustomElsaStudioElements();
    options.RootComponents.MaxJSRootComponents = 1000;
});

// Add your existing services
builder.Services.AddControllersWithViews();

// Add Elsa Server (workflow runtime and API)
builder.Services.AddElsa(elsa =>
{
    elsa
        .UseWorkflowManagement(management =>
        {
            management.UseEntityFrameworkCore(ef =>
                ef.UsePostgreSql(builder.Configuration.GetConnectionString("ElsaDatabase")));
        })
        .UseWorkflowRuntime(runtime =>
        {
            runtime.UseEntityFrameworkCore(ef =>
                ef.UsePostgreSql(builder.Configuration.GetConnectionString("ElsaDatabase")));
        })
        .UseWorkflowsApi()
        .UseHttp();
});

// Add Elsa Studio 3.7.0 host services.
builder.Services.AddCore();
builder.Services.AddShell(options => builder.Configuration.GetSection("Shell").Bind(options));
builder.Services.AddRemoteBackend(new BackendApiConfig
{
    ConfigureBackendOptions = options => builder.Configuration.GetSection("Backend").Bind(options)
});
builder.Services.AddDashboardModule();
builder.Services.AddWorkflowsModule();

var app = builder.Build();

// Configure middleware pipeline
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

// Authentication and authorization
app.UseAuthentication();
app.UseAuthorization();

// Map Elsa API endpoints under /elsa
app.UseWorkflowsApi();

// Map Blazor hub and Studio UI
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");  // Or your Blazor root page

app.Run();
```

Configure the Studio backend URL in `appsettings.json`:

```json
{
  "Shell": {
    "DisableAuthorization": false
  },
  "Backend": {
    "Url": "https://localhost:5001/elsa/api"
  }
}
```

### Step 3: Create Blazor Host Page

Create or update `Pages/_Host.cshtml`:

```html
@page "/"
@namespace YourApp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Workflow Designer</title>
    <base href="~/" />
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    <link href="css/site.css" rel="stylesheet" />
    
    <!-- Elsa Studio styles -->
    <link href="_content/Elsa.Studio.Core.BlazorWasm/css/elsa-studio.css" rel="stylesheet" />
</head>
<body>
    <component type="typeof(App)" render-mode="ServerPrerendered" />

    <div id="blazor-error-ui">
        <environment include="Staging,Production">
            An error has occurred. This application may no longer respond until reloaded.
        </environment>
        <environment include="Development">
            An unhandled exception has occurred. See browser dev tools for details.
        </environment>
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>

    <script src="_framework/blazor.server.js"></script>
    
    <!-- Elsa Studio scripts -->
    <script src="_content/Elsa.Studio.Core.BlazorWasm/js/elsa-studio.js"></script>
</body>
</html>
```

### Step 4: Configure App.razor

Update `App.razor` to include Elsa Studio routes:

```razor
@using Elsa.Studio.Core.BlazorWasm

<Router AppAssembly="@typeof(App).Assembly" 
        AdditionalAssemblies="@(new[] { typeof(Studio).Assembly })">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
    <NotFound>
        <PageTitle>Not found</PageTitle>
        <LayoutView Layout="@typeof(MainLayout)">
            <p role="alert">Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>
```

### Step 5: Test the Integration

1. Run your application
2. Navigate to `/workflows` (or the Studio route configured)
3. You should see the Elsa Studio workflow designer

## Authentication Configuration

When hosting Elsa Server and Studio together, authentication must be configured so that:

1. Users can log into the Blazor app
2. Studio API calls to Elsa Server are authorized

### Cookie-Based Authentication (Recommended for Single Process)

This is the simplest approach when both components are in the same process:

```csharp
using Microsoft.AspNetCore.Authentication.Cookies;

var builder = WebApplication.CreateBuilder(args);

// Configure cookie authentication
builder.Services
    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath = "/Account/Login";
        options.LogoutPath = "/Account/Logout";
        options.AccessDeniedPath = "/Account/AccessDenied";
        options.ExpireTimeSpan = TimeSpan.FromHours(8);
        options.SlidingExpiration = true;
    });

builder.Services.AddAuthorization(options =>
{
    // Define policies for workflow management
    options.AddPolicy("WorkflowDesigner", policy =>
        policy.RequireRole("WorkflowAdmin", "WorkflowDesigner"));
    
    options.AddPolicy("WorkflowViewer", policy =>
        policy.RequireRole("WorkflowAdmin", "WorkflowDesigner", "WorkflowViewer"));
});

// Add Blazor and Elsa
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor(options =>
{
    options.RootComponents.RegisterCustomElsaStudioElements();
    options.RootComponents.MaxJSRootComponents = 1000;
});

builder.Services.AddElsa(elsa =>
{
    // Elsa Server configuration
    elsa
        .UseIdentity(identity =>
        {
            // Use ASP.NET Core authentication
            identity.UseAspNetIdentity();
        })
        .UseDefaultAuthentication()
        .UseWorkflowManagement()
        .UseWorkflowRuntime()
        .UseWorkflowsApi();
});

builder.Services.AddCore();
builder.Services.AddShell(options => builder.Configuration.GetSection("Shell").Bind(options));
builder.Services.AddRemoteBackend(new BackendApiConfig
{
    ConfigureBackendOptions = options => builder.Configuration.GetSection("Backend").Bind(options)
});
builder.Services.AddDashboardModule();
builder.Services.AddWorkflowsModule();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();
app.UseWorkflowsApi();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();
```

With this configuration:

* Users log in via your app's login page
* Authentication cookie is automatically sent with Studio → Elsa Server API calls
* Elsa Server validates the cookie and authorizes requests

### Common Authentication Issues

#### Issue 1: 401 Unauthorized on API Calls

**Symptom:** Elsa Studio loads, but API calls to fetch workflow definitions fail with 401 Unauthorized.

**Cause:** Authentication scheme mismatch or missing authentication middleware.

**Solution:**

1. Ensure authentication middleware is added **before** authorization:

   ```csharp
   app.UseAuthentication();  // Must come first
   app.UseAuthorization();
   ```
2. Verify the same authentication scheme is used:

   ```csharp
   // Both must use the same scheme (e.g., Cookies)
   builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);

   builder.Services.AddElsa(elsa =>
   {
       elsa.UseDefaultAuthentication();  // Uses default ASP.NET Core auth
   });
   ```
3. Check that cookies are being sent in Studio API calls (browser DevTools → Network tab)

#### Issue 2: Infinite Login Redirect Loop

**Symptom:** Navigating to Studio redirects to login, which redirects back to Studio, which redirects to login again.

**Cause:** Login path is not excluded from authorization requirements.

**Solution:**

Allow anonymous access to login/logout pages:

```csharp
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages()
    .RequireAuthorization();  // Require auth for all pages

// Except login/logout
app.MapRazorPages()
    .AllowAnonymous()
    .WithName("Account");  // Pages under /Account allow anonymous
```

Or use `[AllowAnonymous]` attribute on login page:

```csharp
[AllowAnonymous]
public class LoginModel : PageModel
{
    // ...
}
```

#### Issue 3: Missing Bearer Token in API Calls

**Symptom:** In a separate services setup, Studio doesn't send authentication token to Elsa Server.

**Cause:** Token forwarding not configured.

**Solution:**

Configure the Studio remote backend to use an authenticating HTTP message handler:

```csharp
builder.Services.AddRemoteBackend(new BackendApiConfig
{
    ConfigureBackendOptions = options => builder.Configuration.GetSection("Backend").Bind(options),
    ConfigureHttpClientBuilder = options =>
    {
        options.AuthenticationHandler = typeof(YourAuthenticatingApiHttpMessageHandler);
    }
});
```

## Implementation: Separate Services Pattern

When running Elsa Server and Studio as separate services, additional configuration is required.

### Elsa Server Configuration

```csharp
// Elsa Server (standalone API service)
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddElsa(elsa =>
{
    elsa
        .UseIdentity(identity =>
        {
            identity.UseConfigurationBasedUserProvider(options => builder.Configuration.GetSection("Identity").Bind(options));
            identity.UseConfigurationBasedApplicationProvider(options => builder.Configuration.GetSection("Identity").Bind(options));
            identity.UseConfigurationBasedRoleProvider(options => builder.Configuration.GetSection("Identity").Bind(options));
        })
        .UseDefaultAuthentication()
        .UseWorkflowManagement()
        .UseWorkflowRuntime()
        .UseWorkflowsApi();
});

// Configure CORS to allow Studio to call API
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowStudio", policy =>
    {
        policy
            .WithOrigins("https://studio.example.com")  // Your Studio URL
            .AllowAnyMethod()
            .AllowAnyHeader()
            .AllowCredentials();  // If using cookies
    });
});

var app = builder.Build();

app.UseCors("AllowStudio");
app.UseAuthentication();
app.UseAuthorization();
app.UseWorkflowsApi();

app.Run();
```

### Elsa Studio Configuration

```csharp
// Elsa Studio (separate Blazor Server app)
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor(options =>
{
    options.RootComponents.RegisterCustomElsaStudioElements();
    options.RootComponents.MaxJSRootComponents = 1000;
});

builder.Services.AddAuthentication(/* Your auth config */);

builder.Services.AddCore();
builder.Services.AddShell(options => builder.Configuration.GetSection("Shell").Bind(options));
builder.Services.AddRemoteBackend(new BackendApiConfig
{
    ConfigureBackendOptions = options => builder.Configuration.GetSection("Backend").Bind(options),
    ConfigureHttpClientBuilder = options =>
    {
        options.AuthenticationHandler = typeof(YourAuthenticatingApiHttpMessageHandler);
    }
});
builder.Services.AddDashboardModule();
builder.Services.AddWorkflowsModule();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();
```

### CORS Configuration

When Studio and Server are on different domains, configure CORS properly:

**Elsa Server (appsettings.json):**

```json
{
  "Elsa": {
    "Cors": {
      "AllowedOrigins": [
        "https://studio.example.com",
        "https://localhost:5002"
      ]
    }
  }
}
```

**Elsa Server (Program.cs):**

```csharp
var allowedOrigins = builder.Configuration.GetSection("Elsa:Cors:AllowedOrigins").Get<string[]>();

builder.Services.AddCors(options =>
{
    options.AddPolicy("ElsaCors", policy =>
    {
        policy
            .WithOrigins(allowedOrigins)
            .AllowAnyMethod()
            .AllowAnyHeader()
            .AllowCredentials();
    });
});
```

## Troubleshooting Common Issues

### Login/401 Issues

**Problem:** Studio loads but shows "Unauthorized" or redirects to login repeatedly.

**Diagnosis:**

1. Check browser DevTools → Network tab
2. Look at API calls from Studio to Elsa Server
3. Check response status codes and headers

**Common Causes:**

1. **Mismatched authentication scheme:**
   * Studio uses cookies, Server expects Bearer tokens
   * Fix: Align both to use the same scheme
2. **Authentication middleware missing or in wrong order:**

   ```csharp
   // Wrong order
   app.UseAuthorization();  // ❌ Before authentication
   app.UseAuthentication();

   // Correct order
   app.UseAuthentication();  // ✅ First
   app.UseAuthorization();
   ```
3. **CORS blocking credentials:**

   ```csharp
   // Must include AllowCredentials for cookie forwarding
   policy.AllowCredentials();
   ```

### Token/Cookie Not Forwarded

**Problem:** User is authenticated in Studio, but API calls don't include authentication.

**Solution:** Configure the Studio remote backend to use an authenticating HTTP message handler:

```csharp
builder.Services.AddHttpContextAccessor();

builder.Services.AddRemoteBackend(new BackendApiConfig
{
    ConfigureBackendOptions = options => builder.Configuration.GetSection("Backend").Bind(options),
    ConfigureHttpClientBuilder = options =>
    {
        options.AuthenticationHandler = typeof(YourAuthenticatingApiHttpMessageHandler);
    }
});

// Implement YourAuthenticatingApiHttpMessageHandler to add only the
// trusted bearer token or cookie values required by your Elsa Server.
```

## Minimal Conceptual Example

Here's a complete minimal example of a Blazor Server app with Elsa Studio:

**Program.cs:**

```csharp
using Elsa.Extensions;
using Elsa.Studio.Contracts;
using Elsa.Studio.Core.BlazorServer.Extensions;
using Elsa.Studio.Dashboard.Extensions;
using Elsa.Studio.Extensions;
using Elsa.Studio.Models;
using Elsa.Studio.Shell.Extensions;
using Elsa.Studio.Workflows.Extensions;
using Microsoft.AspNetCore.Authentication.Cookies;

var builder = WebApplication.CreateBuilder(args);

// Blazor
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor(options =>
{
    options.RootComponents.RegisterCustomElsaStudioElements();
    options.RootComponents.MaxJSRootComponents = 1000;
});

// Authentication
builder.Services
    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie();

builder.Services.AddAuthorization();

// Elsa Server (local)
builder.Services.AddElsa(elsa =>
{
    elsa
        .UseDefaultAuthentication()
        .UseWorkflowManagement()
        .UseWorkflowRuntime()
        .UseWorkflowsApi();
});

// Elsa Studio
builder.Services.AddCore();
builder.Services.AddShell(options => builder.Configuration.GetSection("Shell").Bind(options));
builder.Services.AddRemoteBackend(new BackendApiConfig
{
    ConfigureBackendOptions = options => builder.Configuration.GetSection("Backend").Bind(options)
});
builder.Services.AddDashboardModule();
builder.Services.AddWorkflowsModule();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseWorkflowsApi();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();
```

## Security Considerations

For production deployments, follow security best practices:

1. **Always use HTTPS**: Never transmit authentication tokens over HTTP
2. **Configure CORS restrictively**: Only allow known Studio origins
3. **Use short-lived tokens**: Configure appropriate token lifetimes
4. **Implement RBAC**: Restrict workflow design to authorized users only
5. **Audit access**: Log all workflow modifications

For detailed security configuration, see:

* [Security & Hardening Guide](/guides/security)
* [Direct OpenID Connect](/guides/authentication/direct-openid-connect)
* [Disable Authentication in Development](/guides/authentication/disable-authentication) (development only)

## Next Steps

* **Customize Studio**: Configure themes, localization, and plugins
* **Add Custom Activities**: Extend workflow designer with [Custom Activities](/extensibility/custom-activities)
* **Deploy to Production**: Follow [Kubernetes Deployment](/guides/deployment/kubernetes) guide
* **Integrate with Identity Provider**: Set up [Direct OpenID Connect](/guides/authentication/direct-openid-connect)
* **Run Workflows**: Learn about [Running Workflows](/guides/running-workflows)

## Related Documentation

* [Hosting Elsa in an Existing App](/guides/onboarding/hosting-elsa-in-existing-app)
* [Elsa Studio Application Type](/application-types/elsa-studio)
* [Security & Hardening](/guides/security)
* [Direct OpenID Connect](/guides/authentication/direct-openid-connect)
* [Troubleshooting Guide](/guides/troubleshooting)

***

**Last Updated:** 2025-12-02\
**Addresses Issues:** #87


# Message Broker Topology

Message-broker topology is part of the Elsa deployment contract. It determines which queues, topics, subscriptions, and consumers exist, which nodes process them, and which resources can be removed when a node or deployment disappears.

This guide describes the behavior shipped in `release/3.8.0`. Use it when you are splitting API and worker nodes, moving workflow dispatch over a broker, or deciding whether Elsa should create and clean up Azure Service Bus resources.

## Choose the topology path first

Elsa has several broker integrations that solve different problems. Do not assume that configuring one enables the others.

| Integration                                               | Topology owner                                                                                                 | Use it when                                                                                           |
| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| [MassTransit message activities](/activities/masstransit) | MassTransit configures endpoints for registered consumers and message types.                                   | Workflows publish or receive strongly typed application messages.                                     |
| MassTransit workflow dispatcher                           | Elsa creates a default dispatch queue and one queue for each configured dispatcher channel.                    | Workflow execution and stimulus delivery should cross process or node boundaries through MassTransit. |
| Azure Service Bus activities                              | Your Elsa configuration names queues, topics, and subscriptions; Elsa can create missing resources at startup. | A workflow needs Azure Service Bus-specific queue/topic inputs rather than a MassTransit contract.    |
| MassTransit distributed cache                             | Elsa publishes a change-token message and each node consumes it from a temporary endpoint.                     | In-memory caches on multiple nodes must be invalidated after a change.                                |

The MassTransit activity integration and the Azure Service Bus activity module are separate. A workflow using `SendMessage` or `MessageReceived` from the Azure Service Bus activity module does not use the MassTransit-generated consumer topology.

The MassTransit workflow dispatcher is also opt-in. In the classic feature API, enable `UseMassTransitDispatcher()` on the workflow management and workflow runtime features. Registering MassTransit message types or selecting RabbitMQ or Azure Service Bus as a transport does not, by itself, move Elsa's workflow dispatch traffic onto the broker.

## Endpoint naming in release 3.8.0

Elsa configures MassTransit with kebab-case endpoint names. The exact names depend on whether the endpoint is a dispatcher channel, a temporary consumer, or a consumer discovered by MassTransit from a registered message type.

### Workflow dispatcher queues in the classic feature API

In the classic `AddElsa(...)` feature API, when the MassTransit workflow dispatcher is enabled, the transport features explicitly configure the default dispatcher endpoint:

```
elsa-dispatch-workflow-request
```

If you add a dispatcher channel named `high-priority`, the classic transport path creates the corresponding endpoint:

```
elsa-dispatch-workflow-request-high-priority
```

The channel name is kebab-cased before it is appended. Elsa configures the default endpoint and every configured channel endpoint with `DispatchWorkflowRequestConsumer`.

Use channels to separate workload classes that need different broker-level throughput or consumer limits. A channel is only useful when the sending side selects that channel and a worker is configured to consume the corresponding endpoint.

The CShells transport path in `release/3.8.0` calls MassTransit's `ConfigureEndpoints` but does not call Elsa's explicit `SetupWorkflowDispatcherEndpoints` helper. Do not assume that a configured non-default channel queue exists in that hosting path: verify the generated endpoint or provision the channel topology before selecting it.

### Fixed and per-instance Elsa consumers

The dispatcher is not the whole broker topology. The following endpoints are also relevant when the corresponding Elsa features are enabled:

| Endpoint name in the classic transport path               | Role                                                                                       | Lifetime                                                           |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ |
| `elsa-dispatch-stimulus`                                  | Consumes stimulus-dispatch messages and delivers them to the runtime.                      | Fixed consumer endpoint.                                           |
| `<application-instance>-elsa-dispatch-cancel-workflow`    | Consumes workflow-cancellation requests.                                                   | Temporary, per instance.                                           |
| `<application-instance>-elsa-workflow-definition-updates` | Consumes workflow-definition update events so the node refreshes its local registry/cache. | Temporary, per instance; intentionally ignores `DisableConsumers`. |
| `<application-instance>-elsa-trigger-change-token-signal` | Consumes distributed-cache change-token signals.                                           | Temporary, per instance; intentionally ignores `DisableConsumers`. |

For in-memory transport, the temporary consumer name is used without the broker instance prefix. For RabbitMQ and MassTransit Azure Service Bus, the transport prefixes temporary consumer endpoints with the application instance name as described below. MassTransit-generated application-message endpoints are additional topology and depend on the message types registered by the host.

### Temporary consumer endpoints

Temporary consumers are used for node-specific or short-lived concerns such as distributed cache invalidation and cancellation dispatch. With RabbitMQ and MassTransit Azure Service Bus, Elsa prefixes the consumer name with the application instance name:

```
<application-instance>-<consumer-name>
```

The instance prefix is important when each concurrently running node has a unique application instance name. It prevents temporary consumers on different nodes from unintentionally sharing one queue and gives Azure Service Bus's cleanup logic a way to associate a subscription with an application instance.

Configure a stable, unique instance name for each logical node when restarts should reuse its transport entities. The Core `release/3.8.0` source supports `ApplicationInstanceOptions.InstanceName` or an environment variable such as `HOSTNAME`; without a configured name, the default provider generates a random name for each process start. Random names can accumulate abandoned per-instance entities on transports with entity-count limits.

```csharp
builder.Services.Configure<ApplicationInstanceOptions>(options =>
{
    options.InstanceNameEnvironmentVariable = "HOSTNAME";
});
```

The final broker name can also be shortened or normalized to satisfy transport limits, so inspect the broker after startup rather than hard-coding the full name in external tooling.

In-memory MassTransit uses the registered consumer name directly because the transport exists only inside the process. It cannot deliver messages between processes.

### Explicit Azure Service Bus activity endpoints

The non-MassTransit Azure Service Bus activities use the names supplied by the workflow:

* `SendMessage` sends to the `QueueOrTopic` input.
* `MessageReceived` listens to `QueueOrTopic` and, when the input is a topic, its `Subscription` input.

When `CreateQueuesTopicsAndSubscriptions` is enabled (the release default), the Azure Service Bus activity module creates missing configured queues, topics, and subscriptions during startup. It does not provide the MassTransit temporary-endpoint cleanup behavior, so treat these names as application-owned infrastructure and manage their lifecycle separately.

## Place consumers deliberately

Consumer placement is a deployment decision, not just a broker setting.

For a split deployment using the feature API that exposes `MassTransitFeature.DisableConsumers`, keep consumers enabled on worker nodes and disable them on API-focused nodes:

```csharp
builder.Services.AddElsa(elsa => elsa
    .UseMassTransit(massTransit =>
    {
        massTransit.DisableConsumers = appRole == ApplicationRole.Api;
        massTransit.UseRabbitMq(rabbitMqConnectionString);
    }));
```

On a node where consumers are disabled, Elsa filters the registered/manual consumer set and does not configure the workflow-dispatcher endpoints. That node can still publish messages, but it should not be treated as a workflow worker. The release implementation still appends generated `WorkflowMessageConsumer<T>` consumers for message types registered with `AddMessageType<T>()`, so `DisableConsumers` is not a blanket switch for application-message ingress. If API nodes must not receive those messages, register the message activities only on the worker host or use separate host configuration. Ensure that at least one worker node has consumers enabled and has access to the broker and the workflow persistence store.

`DisableConsumers` suppresses registered consumers unless they are registered to ignore the flag. In `release/3.8.0`, this includes both distributed-cache invalidation and workflow-definition updates. API nodes may therefore still host per-instance consumers such as `<application-instance>-elsa-workflow-definition-updates` and `<application-instance>-elsa-trigger-change-token-signal`. Do not remove these consumers unless you have replaced workflow-definition propagation or cache invalidation with another mechanism.

## Temporary resource lifetime and cleanup

`MassTransitOptions.TemporaryQueueTtl` controls the temporary endpoint lifetime used by the broker transports. If it is not configured, the release uses one hour.

| Transport                     | Temporary endpoint behavior                                                                                                       | Operational implication                                                                                                |
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| In-memory                     | Endpoint exists only while the process is running.                                                                                | No cross-process delivery and no broker cleanup.                                                                       |
| RabbitMQ                      | Endpoint is prefixed with the instance name, non-durable, auto-delete, and has a queue-expiration value from `TemporaryQueueTtl`. | A stopped node's endpoint can disappear, but broker policy and expiry still affect when it is removed.                 |
| MassTransit Azure Service Bus | Endpoint is prefixed with the instance name and uses `AutoDeleteOnIdle` from `TemporaryQueueTtl`.                                 | Idle queues/subscriptions can be removed by Azure Service Bus; instance naming is also used by Elsa's orphan handling. |

For example:

```csharp
builder.Services.Configure<MassTransitOptions>(options =>
{
    options.TemporaryQueueTtl = TimeSpan.FromHours(2);
    options.ConcurrentMessageLimit = 16;
    options.PrefetchCount = 32;
});
```

Choose a lifetime that covers expected restarts and transient outages without leaving abandoned resources indefinitely. This option is about temporary consumer endpoints; it does not change the retention of workflow instances, bookmarks, or application messages.

### Azure Service Bus cleanup warning

MassTransit Azure Service Bus has two related cleanup paths in `release/3.8.0`:

1. Heartbeat-timeout handling can remove temporary subscriptions and their forwarded queues for an instance that is no longer alive.
2. `EnableAutomatedSubscriptionCleanup` adds a periodic cleanup service. It starts after two minutes, then runs at the configured interval (seven days by default), using a distributed lock so only one node performs the scan.

The automated scan is not ownership-aware. It scans topics whose names start with `elsa`, deletes empty topics, and removes subscriptions whose forwarded queue cannot be found in the same namespace. The release source explicitly warns that it cannot distinguish Elsa-created topology from other topology and that queues in another namespace appear missing.

Enable this option only when the Azure Service Bus namespace is governed by Elsa's naming and lifecycle rules. Otherwise, leave it disabled and clean up orphaned resources with an ownership-aware operator or deployment process.

## Distributed cache invalidation

`distributedCaching.UseMassTransit()` is not workflow-message processing. When a cache signal is published, Elsa sends a `TriggerChangeTokenSignal` containing the cache key. Each node consumes the signal and invokes its local change-token handler.

The released extensions feature registers the temporary consumer name `elsa-trigger-change-token-signal`; the selected MassTransit transport adds the application-instance prefix and may normalize the final broker entity name.

This endpoint is deliberately node-specific: each node needs one copy of the signal so it can invalidate its own in-memory cache. If you instead configure one shared queue for all nodes, the broker may deliver a signal to only one consumer and other nodes can retain stale data.

Use the [distributed hosting guide](/hosting/distributed-hosting) for the broader cache and locking setup. Use the [MassTransit guide](/activities/masstransit) for application message contracts and generated workflow activities.

## Operations checklist

Before promoting a broker-backed Elsa deployment, verify:

1. The broker account can create and configure the queues, topics, and subscriptions required by the selected integration.
2. Worker nodes, rather than API-only nodes, consume the workflow-dispatcher endpoints.
3. Application instance names are stable across restarts of the same logical node and unique while nodes overlap during a rolling deployment; otherwise temporary endpoints can accumulate or collide.
4. Dispatcher channel names are documented alongside the code that selects them, and the corresponding endpoints exist after startup.
5. Temporary endpoint expiry is compatible with restart and outage windows.
6. Azure automated cleanup is disabled when the namespace contains topology owned by another application.
7. Cache invalidation uses a per-node consumer topology and is tested during a rolling restart.

For a first deployment, inspect the broker after Elsa starts and compare the observed topology with the intended endpoint list. The endpoint names are derived from runtime registrations, so a queue list is often the fastest way to detect a missing transport package, disabled consumer, or unexpected dispatcher channel.


# Clustering

Comprehensive guide to running Elsa Workflows in clustered and distributed production environments, covering architecture patterns, distributed locking, scheduling, and operational best practices.

## Executive Summary

Running Elsa Workflows in a clustered environment is essential for achieving high availability, scalability, and fault tolerance in production deployments. A clustered setup allows multiple Elsa instances to work together, distributing workload across nodes while maintaining consistency and preventing data corruption.

### Why Clustering Matters

**Production Requirements Clustering Solves:**

1. **High Availability**: If one node fails, others continue processing workflows without interruption
2. **Horizontal Scalability**: Handle increased load by adding more nodes rather than scaling vertically
3. **Zero-Downtime Deployments**: Rolling updates with no service interruption
4. **Geographic Distribution**: Deploy nodes across regions for disaster recovery and reduced latency
5. **Load Balancing**: Distribute HTTP requests and background jobs across multiple instances

**Key Challenges Clustering Addresses:**

* **Concurrent Modification**: Preventing multiple nodes from modifying the same workflow instance simultaneously
* **Duplicate Scheduling**: Ensuring timers and scheduled tasks execute only once
* **Cache Consistency**: Keeping in-memory caches synchronized across nodes
* **Race Conditions**: Managing concurrent bookmark resume attempts

Without proper clustering configuration, you may encounter:

* Workflow state corruption from simultaneous updates
* Duplicate timer executions causing repeated notifications or side effects
* Cache inconsistencies leading to stale workflow definitions
* Race conditions when external events trigger workflow resumption

## Conceptual Overview

### Understanding Corruption and Duplication Risks

#### Problem 1: Duplicate Timer Execution

**Scenario:** A workflow with a timer activity (e.g., "Send reminder email in 24 hours") is deployed across 3 nodes.

**Without Clustering:**

```
Time T+24h:
- Node 1 checks: "Timer expired? Yes" → Sends email
- Node 2 checks: "Timer expired? Yes" → Sends email  ❌ Duplicate!
- Node 3 checks: "Timer expired? Yes" → Sends email  ❌ Duplicate!
```

**Result:** Customer receives 3 identical reminder emails instead of 1.

**With Clustering (Quartz.NET Clustering):**

```
Time T+24h:
- Quartz Cluster: Node 1 acquires job lock → Executes task
- Node 2 attempts lock → Already held by Node 1 → Skips
- Node 3 attempts lock → Already held by Node 1 → Skips
```

**Result:** Customer receives exactly 1 email as intended.

#### Problem 2: Concurrent Workflow Modification

**Scenario:** An HTTP workflow receives two simultaneous requests that both attempt to resume the same workflow instance.

**Without Distributed Locking:**

```
Request A arrives at Node 1:
1. Load workflow instance (State: Step 2)
2. Execute Step 3
3. Save workflow instance (State: Step 3)

Request B arrives at Node 2 (simultaneously):
1. Load workflow instance (State: Step 2)  ← Loads stale state!
2. Execute Step 3
3. Save workflow instance (State: Step 3)  ← Overwrites Node 1's changes!
```

**Result:** Workflow execution is corrupted; steps may be skipped or repeated.

**With Distributed Locking:**

```
Request A arrives at Node 1:
1. Acquire lock on workflow instance
2. Load, execute, save
3. Release lock

Request B arrives at Node 2 (simultaneously):
1. Attempt to acquire lock → Blocked (Node 1 holds it)
2. Wait for lock release
3. Lock released → Detect workflow already resumed → Skip or continue appropriately
```

**Result:** Workflow executes correctly without corruption.

#### Problem 3: Cache Invalidation

**Scenario:** An administrator updates a workflow definition in Elsa Studio.

**Without Distributed Cache Invalidation:**

```
Admin updates workflow via Node 1:
- Node 1: Clears local cache, reloads definition ✓
- Node 2: Keeps stale cache ❌ (uses old version)
- Node 3: Keeps stale cache ❌ (uses old version)
```

**Result:** New workflow instances on Node 2 and 3 use outdated definitions.

**With Distributed Cache Invalidation (MassTransit):**

```
Admin updates workflow via Node 1:
- Node 1: Publishes "WorkflowDefinitionChanged" event to message bus
- Node 2: Receives event → Clears local cache ✓
- Node 3: Receives event → Clears local cache ✓
```

**Result:** All nodes use the updated workflow definition immediately.

### How Elsa Mitigates These Risks

Elsa provides four key mechanisms for safe clustering:

#### 1. Bookmark Hashing

Bookmarks (suspension points in workflows) are assigned deterministic hashes based on their properties. When multiple nodes attempt to create the same bookmark, the hash collision is detected, and only one bookmark is persisted.

**Code Reference:** `src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs` - `CreateBookmark` method

```csharp
// Simplified illustration of bookmark hashing
var bookmarkHash = GenerateHash(activityId, payload, correlationId);
var existingBookmark = await FindBookmarkByHash(bookmarkHash);

if (existingBookmark != null)
{
    // Bookmark already exists; don't create duplicate
    return existingBookmark;
}

// Create new bookmark with unique hash
var bookmark = new Bookmark { Hash = bookmarkHash, ... };
await SaveBookmark(bookmark);
```

#### 2. Distributed Locking

The `WorkflowResumer` service acquires a distributed lock before resuming a workflow instance. This ensures only one node processes a resume request at a time.

**Code Reference:** `src/modules/Elsa.Workflows.Runtime/Services/WorkflowResumer.cs`

```csharp
// Simplified illustration from WorkflowResumer
public async Task<ResumeWorkflowResult> ResumeWorkflowAsync(
    string workflowInstanceId,
    string bookmarkId,
    CancellationToken cancellationToken)
{
    // Generate deterministic lock key
    var lockKey = $"workflow:{workflowInstanceId}:bookmark:{bookmarkId}";
    
    // Acquire distributed lock (Redis, PostgreSQL, etc.)
    await using var lockHandle = await _distributedLockProvider
        .AcquireLockAsync(lockKey, timeout: TimeSpan.FromSeconds(30), cancellationToken);
    
    if (lockHandle == null)
    {
        // Another node is already processing this resume
        _logger.LogInformation("Lock acquisition failed; resume already in progress");
        return ResumeWorkflowResult.AlreadyInProgress();
    }
    
    try
    {
        // Safe to resume - we hold the lock
        var result = await ResumeWorkflowCoreAsync(...);
        return result;
    }
    finally
    {
        // Lock automatically released when lockHandle is disposed
    }
}
```

**Lock Providers Supported:**

* **Redis**: Fast, in-memory locking via Medallion.Threading.Redis
* **PostgreSQL**: Database-backed locking via Medallion.Threading.Postgres
* **SQL Server**: Database-backed locking via Medallion.Threading.SqlServer
* **Azure Blob Storage**: Cloud-native locking via Medallion.Threading.Azure

#### 3. Centralized Scheduler (Quartz.NET Clustering)

Quartz.NET clustering ensures scheduled jobs (timers, delays, cron triggers) execute only once across the cluster.

**Code References:**

* `src/modules/Elsa.Scheduling/Services/DefaultBookmarkScheduler.cs` - Enqueues bookmark resume tasks
* `src/modules/Elsa.Scheduling/Tasks/ResumeWorkflowTask.cs` - Quartz job that resumes workflows

**How It Works:**

1. `DefaultBookmarkScheduler` creates a Quartz job for each scheduled bookmark
2. Quartz stores job metadata in a shared database
3. At execution time, nodes compete for a database lock
4. The node that acquires the lock executes the job; others skip it
5. Failed nodes' jobs are recovered by surviving nodes (failover)

#### 4. Distributed Cache Invalidation

When workflow definitions or other cached data changes, MassTransit publishes cache invalidation events to all nodes via a message broker (RabbitMQ, Azure Service Bus, etc.).

**Message Flow:**

```
Node 1 (Admin updates workflow) → Publish event to RabbitMQ
                                    ↓
                    ┌───────────────┼───────────────┐
                    ↓               ↓               ↓
                  Node 1          Node 2          Node 3
            (clear cache)   (clear cache)   (clear cache)
```

## Architecture Patterns and Deployment Models

### Pattern 1: Shared Database + Distributed Locks

**Best for:** Most production scenarios with moderate to high traffic

```
┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│  Elsa Node  │  │  Elsa Node  │  │  Elsa Node  │
│   (Pod 1)   │  │   (Pod 2)   │  │   (Pod 3)   │
└──────┬──────┘  └──────┬──────┘  └──────┬──────┘
       │                │                │
       └────────────────┼────────────────┘
                        ↓
            ┌───────────────────────┐
            │  PostgreSQL Database  │
            │  - Workflow instances │
            │  - Bookmarks          │
            │  - Distributed locks  │
            │  - Quartz tables      │
            └───────────────────────┘
                        ↑
                        │
            ┌───────────────────────┐
            │  Redis (optional)     │
            │  - Distributed locks  │
            │  - Session cache      │
            └───────────────────────┘
                        ↑
                        │
            ┌───────────────────────┐
            │  RabbitMQ             │
            │  - Cache invalidation │
            │  - Event pub/sub      │
            └───────────────────────┘
```

**Configuration:**

* All nodes connect to the same database
* Distributed runtime enabled: `runtime.UseDistributedRuntime()`
* Distributed locks via Redis or PostgreSQL
* Quartz clustering enabled for scheduled tasks
* MassTransit for cache invalidation

**Pros:**

* Simple architecture
* Easy to scale horizontally
* No single point of failure (stateless nodes)

**Cons:**

* Database becomes a bottleneck at extreme scale
* Requires careful database tuning

### Pattern 2: Leader-Election Scheduler

**Best for:** Environments where you want precise control over scheduling overhead

```
┌─────────────────────┐
│  Scheduler Node     │ ← Leader (elected or manually designated)
│  - Quartz scheduler │
│  - No HTTP endpoint │
└─────────┬───────────┘
          │ Schedules bookmark resume tasks
          ↓
┌──────────────────────────────────────┐
│  Worker Nodes (HTTP-only)            │
│  - Handle HTTP requests              │
│  - Execute workflows                 │
│  - No Quartz scheduler               │
└──────────────────────────────────────┘
```

**Configuration:**

**Scheduler Node:**

```csharp
builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowRuntime(runtime => runtime.UseDistributedRuntime());
    elsa.UseScheduling(scheduling => scheduling.UseQuartzScheduler());
    elsa.UseQuartz(quartz => quartz.UsePostgreSql(connectionString));
    // Don't expose HTTP endpoints (or expose for admin only)
});
```

**Worker Nodes:**

```csharp
builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowRuntime(runtime => runtime.UseDistributedRuntime());
    elsa.UseWorkflowsApi();  // Handle HTTP requests
    // Don't call UseScheduling() - no scheduler on workers
});
```

**Pros:**

* Centralized scheduling (easier to monitor)
* Workers focused on request handling
* Lower resource usage on workers

**Cons:**

* Scheduler is a single point of failure (mitigate with active-standby setup)
* More complex deployment configuration

### Pattern 3: Quartz Clustering (All-Nodes-Participate)

**Best for:** Simplicity and automatic failover

```
┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│  Node 1     │  │  Node 2     │  │  Node 3     │
│  - Quartz   │  │  - Quartz   │  │  - Quartz   │
│  - HTTP     │  │  - HTTP     │  │  - HTTP     │
└──────┬──────┘  └──────┬──────┘  └──────┬──────┘
       └────────────────┼────────────────┘
                        ↓
            ┌───────────────────────┐
            │  PostgreSQL           │
            │  - Quartz tables      │
            │  - Cluster locks      │
            └───────────────────────┘
```

**Configuration:**

```csharp
builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowRuntime(runtime => runtime.UseDistributedRuntime());
    elsa.UseScheduling(scheduling => scheduling.UseQuartzScheduler());
    elsa.UseQuartz(quartz => 
    {
        quartz.UsePostgreSql(connectionString);
        // Clustering enabled automatically
    });
});
```

**Pros:**

* Simple configuration (same for all nodes)
* Automatic failover (if Node 1 crashes, Node 2 picks up its jobs)
* No single point of failure

**Cons:**

* Every node runs Quartz scheduler (slightly higher resource usage)
* More database queries for cluster coordination

**Recommendation:** Use this pattern unless you have specific reasons to use leader-election.

### Pattern 4: External Scheduler

**Best for:** Multi-tenant environments or complex orchestration needs

```
┌───────────────────────┐
│  External Scheduler   │ ← Kubernetes CronJob, Azure Functions, AWS Lambda
│  - Triggers workflows │
│  - No Elsa runtime    │
└───────────┬───────────┘
            │ HTTP API calls
            ↓
┌──────────────────────────────────────┐
│  Elsa Worker Nodes                   │
│  - REST API endpoints                │
│  - Execute workflows                 │
│  - No internal scheduler             │
└──────────────────────────────────────┘
```

**Example: Kubernetes CronJob**

```yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: daily-report-trigger
spec:
  schedule: "0 2 * * *"  # Daily at 2 AM
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: trigger
              image: curlimages/curl:latest
              command:
                - /bin/sh
                - -c
                - |
                  curl -X POST https://elsa.example.com/elsa/api/workflow-instances \
                    -H "Content-Type: application/json" \
                    -d '{"definitionId": "DailyReport", "input": {}}'
          restartPolicy: OnFailure
```

**Pros:**

* No Quartz dependency
* Leverage platform-native scheduling (Kubernetes, cloud functions)
* Easier multi-cloud deployments

**Cons:**

* External system must remain operational
* More complex integration (API authentication, error handling)
* No built-in bookmark scheduling (must implement externally)

## Practical Configuration

### Configuring Distributed Locks

#### Option 1: Redis-Based Locking (Recommended for Performance)

**Prerequisites:**

* Redis 6.0+ deployed and accessible
* NuGet package: `Medallion.Threading.Redis`

**Configuration Example:**

```csharp
using Elsa.Extensions;
using Medallion.Threading.Redis;
using StackExchange.Redis;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowRuntime(runtime =>
    {
        runtime.UseDistributedRuntime();
        
        runtime.DistributedLockProvider = serviceProvider =>
        {
            var redisConnectionString = builder.Configuration.GetConnectionString("Redis");
            var connection = ConnectionMultiplexer.Connect(redisConnectionString);
            
            return new RedisDistributedSynchronizationProvider(
                connection.GetDatabase(),
                options =>
                {
                    // Lock expires after 30 seconds if not released
                    options.Expiry(TimeSpan.FromSeconds(30));
                    
                    // Minimum time before lock can expire (prevents premature expiration)
                    options.MinimumDatabaseExpiry(TimeSpan.FromSeconds(10));
                });
        };
    });
});

var app = builder.Build();
app.Run();
```

**Connection String Example:**

```
redis-host:6379,password=YOUR_PASSWORD,ssl=False,abortConnect=False,connectTimeout=5000
```

**See:** `examples/redis-lock-setup.md` for detailed configuration and troubleshooting.

#### Option 2: PostgreSQL-Based Locking (No Additional Infrastructure)

**Prerequisites:**

* PostgreSQL 12+ (same database as Elsa workflow storage)
* NuGet package: `Medallion.Threading.Postgres`

**Configuration Example:**

```csharp
using Elsa.Extensions;
using Medallion.Threading.Postgres;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowRuntime(runtime =>
    {
        runtime.UseDistributedRuntime();
        
        runtime.DistributedLockProvider = serviceProvider =>
            new PostgresDistributedSynchronizationProvider(
                builder.Configuration.GetConnectionString("PostgreSql"),
                options =>
                {
                    // Keep connection alive with periodic keepalive
                    options.KeepaliveCadence(TimeSpan.FromMinutes(5));
                    
                    // Use connection multiplexing for efficiency
                    options.UseMultiplexing();
                });
    });
});

var app = builder.Build();
app.Run();
```

**Connection String Example:**

```
Server=postgres-host;Port=5432;Database=elsa;User Id=elsa;Password=YOUR_PASSWORD;MaxPoolSize=100
```

**Pros vs Redis:**

* ✅ No additional infrastructure required
* ✅ Uses existing database connection
* ❌ Slower lock acquisition (disk I/O vs in-memory)
* ❌ Adds load to database

**Medallion.Threading Usage in Elsa Core:**

Elsa uses Medallion.Threading abstractions to remain agnostic to the lock provider. The `IDistributedLockProvider` interface is implemented by all Medallion providers:

* `RedisDistributedSynchronizationProvider`
* `PostgresDistributedSynchronizationProvider`
* `SqlDistributedSynchronizationProvider`
* `AzureDistributedSynchronizationProvider`

To use a different provider, simply register it as shown above. Elsa's `WorkflowResumer` will automatically use the registered provider.

### Configuring Quartz Clustering

**Example quartz.properties:**

```properties
# Cluster instance configuration
quartz.scheduler.instanceName = ElsaQuartzCluster
quartz.scheduler.instanceId = AUTO

# Thread pool
quartz.threadPool.type = Quartz.Simpl.SimpleThreadPool, Quartz
quartz.threadPool.threadCount = 10

# Persistent job store with clustering
quartz.jobStore.type = Quartz.Impl.AdoJobStore.JobStoreTX, Quartz
quartz.jobStore.dataSource = default
quartz.jobStore.tablePrefix = qrtz_
quartz.jobStore.driverDelegateType = Quartz.Impl.AdoJobStore.PostgreSQLDelegate, Quartz

# Enable clustering
quartz.jobStore.clustered = true
quartz.jobStore.clusterCheckinInterval = 20000
quartz.jobStore.clusterCheckinMisfireThreshold = 60000

# PostgreSQL data source
quartz.dataSource.default.provider = Npgsql
quartz.dataSource.default.connectionString = Server=localhost;Database=elsa;User Id=elsa;Password=YOUR_PASSWORD

# Serialization
quartz.serializer.type = json
```

**Configuration in Program.cs:**

```csharp
using Elsa.Extensions;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddElsa(elsa =>
{
    // Enable distributed runtime
    elsa.UseWorkflowRuntime(runtime => runtime.UseDistributedRuntime());
    
    // Enable Quartz scheduling
    elsa.UseScheduling(scheduling => scheduling.UseQuartzScheduler());
    
    // Configure Quartz with PostgreSQL and clustering
    elsa.UseQuartz(quartz =>
    {
        // This automatically enables clustering
        quartz.UsePostgreSql(builder.Configuration.GetConnectionString("PostgreSql"));
    });
});

// Configure Quartz service
builder.Services.AddQuartzHostedService(options =>
{
    options.WaitForJobsToComplete = true;
});

var app = builder.Build();
app.Run();
```

**Environment Variables:**

```bash
QUARTZ__CLUSTERED=true
QUARTZ__INSTANCENAME=ElsaQuartzCluster
QUARTZ__SCHEDULER_INSTANCEID=AUTO
CONNECTIONSTRINGS__POSTGRESQL="Server=postgres-host;Database=elsa;User Id=elsa;Password=YOUR_PASSWORD"
```

**See:** `examples/quartz-cluster-config.md` for detailed configuration options.

### Kubernetes Configuration

**Minimal Deployment Snippet:**

See `examples/k8s-deployment.yaml` for a complete example with:

* Deployment with 3 replicas
* Service (ClusterIP, no session affinity)
* HorizontalPodAutoscaler
* Pod Disruption Budget
* Health probes (liveness, readiness, startup)

**Key Points:**

* **No Sticky Sessions Required**: Elsa's distributed runtime manages state externally, so requests can be routed to any node
* **Readiness Probes**: Use `/health/ready` endpoint to ensure pods are ready before receiving traffic
* **Liveness Probes**: Use `/health/live` endpoint to restart unhealthy pods
* **Anti-Affinity**: Spread pods across nodes for high availability
* **Resource Limits**: Set appropriate CPU/memory limits to prevent resource contention

**Ingress Configuration:**

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: elsa-ingress
  namespace: elsa-workflows
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
    # No session affinity required
    nginx.ingress.kubernetes.io/affinity: "none"
spec:
  ingressClassName: nginx
  rules:
    - host: elsa.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: elsa-server
                port:
                  number: 80
```

**Helm Values:**

See `examples/helm-values.yaml` for an annotated Helm values file with:

* Multiple replicas configuration
* Database, Redis, and RabbitMQ settings
* Distributed runtime and locking configuration
* Quartz clustering settings
* HPA and resource limits
* Health probe configurations

### Docker Compose Development Example

For local development and testing clustering behavior:

```yaml
version: '3.8'

services:
  elsa-node1:
    image: elsaworkflows/elsa-server-v3:latest
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - CONNECTIONSTRINGS__POSTGRESQL=Server=postgres;Database=elsa;User Id=elsa;Password=elsa
      - REDIS__CONNECTIONSTRING=redis:6379
      - RABBITMQ__CONNECTIONSTRING=amqp://guest:guest@rabbitmq:5672/
      - ELSA__RUNTIME__TYPE=Distributed
      - QUARTZ__CLUSTERED=true
    ports:
      - "5001:8080"
    depends_on:
      - postgres
      - redis
      - rabbitmq

  elsa-node2:
    image: elsaworkflows/elsa-server-v3:latest
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - CONNECTIONSTRINGS__POSTGRESQL=Server=postgres;Database=elsa;User Id=elsa;Password=elsa
      - REDIS__CONNECTIONSTRING=redis:6379
      - RABBITMQ__CONNECTIONSTRING=amqp://guest:guest@rabbitmq:5672/
      - ELSA__RUNTIME__TYPE=Distributed
      - QUARTZ__CLUSTERED=true
    ports:
      - "5002:8080"
    depends_on:
      - postgres
      - redis
      - rabbitmq

  elsa-node3:
    image: elsaworkflows/elsa-server-v3:latest
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - CONNECTIONSTRINGS__POSTGRESQL=Server=postgres;Database=elsa;User Id=elsa;Password=elsa
      - REDIS__CONNECTIONSTRING=redis:6379
      - RABBITMQ__CONNECTIONSTRING=amqp://guest:guest@rabbitmq:5672/
      - ELSA__RUNTIME__TYPE=Distributed
      - QUARTZ__CLUSTERED=true
    ports:
      - "5003:8080"
    depends_on:
      - postgres
      - redis
      - rabbitmq

  postgres:
    image: postgres:16-alpine
    environment:
      - POSTGRES_DB=elsa
      - POSTGRES_USER=elsa
      - POSTGRES_PASSWORD=elsa
    ports:
      - "5432:5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    command: redis-server --appendonly yes

  rabbitmq:
    image: rabbitmq:3-management-alpine
    ports:
      - "5672:5672"
      - "15672:15672"  # Management UI

volumes:
  postgres-data:
```

**Testing Commands:**

```bash
# Start cluster
docker-compose up -d

# Check all nodes are healthy
curl http://localhost:5001/health/ready
curl http://localhost:5002/health/ready
curl http://localhost:5003/health/ready

# Create a workflow with a timer
curl -X POST http://localhost:5001/elsa/api/workflow-instances \
  -H "Content-Type: application/json" \
  -d '{"definitionId": "TimerTest"}'

# Check Quartz cluster state
docker-compose exec postgres psql -U elsa -c "SELECT * FROM qrtz_scheduler_state;"

# Check logs for distributed lock activity
docker-compose logs -f elsa-node1 | grep -i "lock"
```

## Operational Topics

### Metrics to Monitor

**Workflow Execution Metrics:**

* Active workflow instances
* Workflows completed per minute
* Workflow execution failures
* Average workflow execution time

**Distributed Locking Metrics:**

* Lock acquisition time (P50, P95, P99)
* Lock acquisition failures
* Lock hold duration
* Lock contention rate

**Quartz Scheduling Metrics:**

* Scheduled jobs count
* Job execution rate
* Job misfires
* Scheduler heartbeat intervals

**System Metrics:**

* CPU usage per pod
* Memory usage per pod
* Database connection pool utilization
* Redis connection pool utilization

**Database Metrics:**

* Query execution time
* Connection pool exhaustion
* Deadlocks
* Lock wait time

### Log Levels and Structured Logging

**Recommended Log Levels:**

**Production:**

```json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Elsa": "Information",
      "Elsa.Workflows.Runtime": "Information",
      "Quartz": "Warning"
    }
  }
}
```

**Debugging Clustering Issues:**

```json
{
  "Logging": {
    "LogLevel": {
      "Elsa.Workflows.Runtime.Services.WorkflowResumer": "Debug",
      "Elsa.Scheduling": "Debug",
      "Quartz": "Debug"
    }
  }
}
```

**Key Log Messages to Watch:**

**Successful Lock Acquisition:**

```
[INF] Acquired distributed lock for workflow instance {WorkflowInstanceId}
```

**Lock Acquisition Failure (expected in clusters):**

```
[INF] Lock acquisition failed; resume already in progress for workflow {WorkflowInstanceId}
```

**Quartz Job Execution:**

```
[INF] Quartz job executed: ResumeWorkflowTask for bookmark {BookmarkId}
```

**Cache Invalidation:**

```
[INF] Received cache invalidation event for workflow definition {DefinitionId}
```

### Troubleshooting Common Issues

#### Issue: Duplicate Workflow Executions

**Symptoms:**

* Timers firing multiple times
* Duplicate notifications or side effects
* Multiple log entries for the same workflow execution

**Diagnosis:**

```bash
# Check if distributed runtime is enabled
kubectl logs <pod-name> | grep "UseDistributedRuntime"

# Check Quartz cluster state
kubectl exec -it postgres-pod -- psql -U elsa -c "SELECT * FROM qrtz_scheduler_state;"

# Check distributed lock acquisitions
kubectl logs <pod-name> | grep -i "lock acquisition"
```

**Solutions:**

1. Verify `runtime.UseDistributedRuntime()` is called in configuration
2. Ensure Quartz clustering is enabled (`quartz.jobStore.clustered = true`)
3. Check distributed lock provider is registered and accessible
4. Verify all nodes use the same database and Redis instance

#### Issue: Bookmark Not Found

**Symptoms:**

* Scheduled tasks fail with "Bookmark not found" error
* Workflows not resuming at expected time

**Diagnosis:**

```sql
-- Check bookmarks table
SELECT * FROM elsa.bookmarks WHERE workflow_instance_id = '<instance-id>';

-- Check Quartz scheduled jobs
SELECT * FROM qrtz_triggers WHERE trigger_group = 'Elsa.Scheduling';
```

**Solutions:**

1. Check database connectivity from all nodes
2. Verify clock synchronization across nodes (NTP)
3. Ensure time zones are configured consistently
4. Check for database replication lag (if using replicas)

#### Issue: Lock Acquisition Timeouts

**Symptoms:**

* Workflows stuck in "Suspended" state
* Logs show "Failed to acquire lock after timeout"

**Diagnosis:**

```bash
# Check Redis connectivity
redis-cli -h redis-host PING

# Check for stuck locks in Redis
redis-cli --scan --pattern "workflow:*" | wc -l

# Check PostgreSQL locks
SELECT * FROM pg_locks WHERE locktype = 'advisory';
```

**Solutions:**

1. Increase lock acquisition timeout
2. Check Redis/database connectivity and latency
3. Verify lock expiration is configured to prevent stuck locks
4. Clear stale locks (use with caution):

   ```bash
   # Redis
   redis-cli --scan --pattern "workflow:*" | xargs redis-cli DEL

   # PostgreSQL (Medallion.Threading creates advisory locks, they auto-release)
   ```

#### Issue: Cache Inconsistencies

**Symptoms:**

* Nodes using different workflow definitions
* Changes not reflecting immediately on all nodes

**Diagnosis:**

```bash
# Check MassTransit/RabbitMQ connectivity
kubectl logs <pod-name> | grep -i "masstransit"

# Check RabbitMQ queues
kubectl exec -it rabbitmq-pod -- rabbitmqctl list_queues
```

**Solutions:**

1. Verify `elsa.UseDistributedCache(dc => dc.UseMassTransit())` is configured
2. Check RabbitMQ connectivity from all nodes
3. Verify message broker is operational
4. Restart all pods to force cache refresh

### Retention and Cleanup

**Workflow Instance Cleanup:**

Old completed or faulted workflow instances should be cleaned up periodically:

```csharp
// Configure retention policy in Elsa
builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowManagement(management =>
    {
        management.UseWorkflowInstanceRetention(retention =>
        {
            retention.RetentionPeriod = TimeSpan.FromDays(30);
            retention.SweepInterval = TimeSpan.FromHours(1);
        });
    });
});
```

**Manual Cleanup (SQL):**

```sql
-- Delete completed workflows older than 30 days
DELETE FROM elsa.workflow_instances
WHERE status = 'Completed'
  AND finished_at < NOW() - INTERVAL '30 days';

-- Delete faulted workflows older than 90 days
DELETE FROM elsa.workflow_instances
WHERE status = 'Faulted'
  AND finished_at < NOW() - INTERVAL '90 days';
```

**Quartz Cleanup:**

Quartz automatically cleans up completed jobs, but you may want to clean up old execution history:

```sql
-- Delete old fired triggers (already executed)
DELETE FROM qrtz_fired_triggers
WHERE fired_time < EXTRACT(EPOCH FROM NOW() - INTERVAL '7 days') * 1000;
```

**Bookmark Cleanup:**

Orphaned bookmarks (no associated workflow instance) should be cleaned up:

```sql
-- Find orphaned bookmarks
SELECT b.* FROM elsa.bookmarks b
LEFT JOIN elsa.workflow_instances wi ON b.workflow_instance_id = wi.id
WHERE wi.id IS NULL;

-- Delete orphaned bookmarks
DELETE FROM elsa.bookmarks
WHERE workflow_instance_id NOT IN (SELECT id FROM elsa.workflow_instances);
```

## Validation Checklist for Cluster Behavior

Use this checklist to validate your clustering setup in a test environment:

### 1. Distributed Runtime Validation

* [ ] Deploy at least 3 Elsa nodes
* [ ] Create an HTTP workflow with a suspend/resume pattern
* [ ] Send simultaneous requests to resume the same workflow instance via different nodes
* [ ] Verify in logs that only one node acquires the lock and processes the resume
* [ ] Verify the workflow completes successfully without state corruption

**Test Command:**

```bash
# Send simultaneous requests to different nodes
for i in {1..10}; do
  curl -X POST http://node1/elsa/api/workflow-instances/<id>/resume &
  curl -X POST http://node2/elsa/api/workflow-instances/<id>/resume &
  curl -X POST http://node3/elsa/api/workflow-instances/<id>/resume &
done
wait

# Check logs for lock acquisitions
kubectl logs -l app=elsa-server | grep "Acquired distributed lock"
```

### 2. Scheduled Task Validation

* [ ] Create a workflow with a timer (e.g., delay 30 seconds)
* [ ] Start the workflow on Node 1
* [ ] Monitor all nodes' logs
* [ ] Verify that exactly one node executes the timer resume
* [ ] Check Quartz scheduler state in database

**Test Command:**

```bash
# Create workflow with timer
curl -X POST http://node1/elsa/api/workflow-instances \
  -H "Content-Type: application/json" \
  -d '{"definitionId": "TimerWorkflow"}'

# Monitor all nodes
kubectl logs -l app=elsa-server -f | grep "ResumeWorkflowTask"

# Check Quartz state
kubectl exec -it postgres-pod -- psql -U elsa -c "SELECT * FROM qrtz_scheduler_state;"
```

### 3. Cache Invalidation Validation

* [ ] Connect to Node 1's Elsa Studio
* [ ] Update a workflow definition
* [ ] Immediately create a new workflow instance on Node 2
* [ ] Verify Node 2 uses the updated definition (not cached old version)
* [ ] Check logs for cache invalidation events on all nodes

**Test Command:**

```bash
# Update workflow via Node 1
curl -X PUT http://node1/elsa/api/workflow-definitions/<id> \
  -H "Content-Type: application/json" \
  -d '{...updated definition...}'

# Create instance on Node 2
curl -X POST http://node2/elsa/api/workflow-instances \
  -H "Content-Type: application/json" \
  -d '{"definitionId": "<id>"}'

# Check cache invalidation logs
kubectl logs -l app=elsa-server | grep "cache invalidation"
```

### 4. Failover Validation

* [ ] Start a workflow with a timer scheduled 5 minutes in the future
* [ ] Note which node is scheduled to execute it (check Quartz)
* [ ] Kill that node before the timer fires
* [ ] Verify another node picks up and executes the scheduled task
* [ ] Check Quartz for failover recovery in logs

**Test Command:**

```bash
# Start workflow with delayed timer
curl -X POST http://node1/elsa/api/workflow-instances \
  -H "Content-Type: application/json" \
  -d '{"definitionId": "DelayedTimerWorkflow"}'

# Check which node owns the scheduled job
kubectl exec -it postgres-pod -- psql -U elsa -c \
  "SELECT * FROM qrtz_fired_triggers;"

# Kill the owning node
kubectl delete pod elsa-server-<pod-id>

# Wait for timer to fire and verify another node executed it
kubectl logs -l app=elsa-server | grep "ResumeWorkflowTask executed"
```

### 5. High Availability Validation

* [ ] Deploy cluster with 3 nodes
* [ ] Generate continuous load (workflow executions)
* [ ] Perform rolling restart of all nodes
* [ ] Verify zero failed workflow executions during restart
* [ ] Check that readiness probes prevent traffic to restarting nodes

**Test Command:**

```bash
# Generate load
while true; do
  curl -X POST http://elsa-service/elsa/api/workflow-instances \
    -H "Content-Type: application/json" \
    -d '{"definitionId": "TestWorkflow"}'
  sleep 1
done &

# Perform rolling restart
kubectl rollout restart deployment/elsa-server

# Monitor
kubectl rollout status deployment/elsa-server
kubectl logs -l app=elsa-server --tail=100
```

### 6. Distributed Lock Validation

* [ ] Enable debug logging for `Elsa.Workflows.Runtime.Services.WorkflowResumer`
* [ ] Trigger concurrent workflow resumes
* [ ] Check logs for lock acquisition and release messages
* [ ] Verify lock acquisition times are reasonable (< 100ms for Redis, < 500ms for DB)
* [ ] Confirm no deadlocks or stuck locks

**Test Command:**

```bash
# Enable debug logging (update configmap or environment variable)
kubectl set env deployment/elsa-server \
  LOGGING__LOGLEVEL__ELSA_WORKFLOWS_RUNTIME_SERVICES_WORKFLOWRESUMER=Debug

# Trigger concurrent resumes
for i in {1..50}; do
  curl -X POST http://elsa-service/elsa/api/workflow-instances/<id>/resume &
done
wait

# Analyze logs
kubectl logs -l app=elsa-server | grep -E "(Acquired|Released) distributed lock"
```

## Security and Networking

### Database Access Security

**Recommendations:**

1. **Use TLS/SSL for database connections:**

   ```
   Server=postgres-host;Database=elsa;User Id=elsa;Password=...;SSLMode=Require
   ```
2. **Restrict database access to Elsa nodes only:**
   * Use Kubernetes Network Policies
   * Configure database firewall rules (cloud-managed databases)
3. **Use dedicated database users with minimal permissions:**

   ```sql
   CREATE USER elsa WITH PASSWORD 'secure_password';
   GRANT CONNECT ON DATABASE elsa TO elsa;
   GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO elsa;
   ```
4. **Rotate credentials regularly:**
   * Use external secret management (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault)
   * Implement automated rotation policies

### Network Latency Considerations

**Impact on Clustering:**

* Distributed lock acquisition time increases with latency
* Quartz cluster check-ins may timeout with high latency
* Cache invalidation events delayed

**Recommendations:**

1. **Co-locate Elsa nodes and dependencies in the same region/AZ**
2. **Monitor network latency between Elsa nodes and:**
   * Database (< 10ms recommended)
   * Redis (< 5ms recommended)
   * RabbitMQ (< 10ms recommended)
3. **Adjust timeouts if cross-region deployment is unavoidable:**

   ```csharp
   // Increase lock acquisition timeout for high-latency environments
   var lockHandle = await _distributedLockProvider.AcquireLockAsync(
       lockKey,
       timeout: TimeSpan.FromSeconds(60),  // Increased from 30s
       cancellationToken);
   ```
4. **Use database connection pooling:**

   ```
   Server=postgres-host;Database=elsa;MaxPoolSize=100;Connection Idle Lifetime=300
   ```

### Time Zone Considerations for Timers

**Issue:** Scheduled workflows may execute at incorrect times if nodes have different time zones.

**Recommendations:**

1. **Ensure all nodes use UTC:**

   ```dockerfile
   # Dockerfile
   ENV TZ=UTC
   RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
   ```
2. **Configure time zone in Kubernetes:**

   ```yaml
   env:
     - name: TZ
       value: "UTC"
   ```
3. **Store all timestamps in UTC in the database**
4. **Convert to user's local time in the UI/API layer**

### Tokenized Resume URL Security

**Code Reference:** `src/modules/Elsa.Http/Extensions/BookmarkExecutionContextExtensions.cs` - `GenerateBookmarkTriggerUrl`

When workflows are suspended waiting for HTTP requests, Elsa can generate tokenized URLs that allow external systems to resume the workflow:

```csharp
// Example: HTTP endpoint activity generates a resume URL
var resumeUrl = context.GenerateBookmarkTriggerUrl();
// Result: https://elsa.example.com/workflows/resume/{token}
```

**Security Considerations:**

1. **Tokens are opaque and unguessable:**
   * Generated using cryptographically secure random number generator
   * Typically 32-64 characters long
2. **Tokens should be single-use:**
   * Elsa automatically invalidates tokens after workflow resumes
   * Replay attacks prevented
3. **Use HTTPS for resume URLs:**
   * Never send tokens over unencrypted HTTP
   * Configure TLS/SSL on ingress controller
4. **Token expiration:**
   * Configure bookmark expiration to automatically clean up old tokens
   * Expired bookmarks cannot be used to resume workflows
5. **Audit logging:**
   * Log all resume attempts (successful and failed)
   * Monitor for unusual patterns (repeated resume attempts, token scanning)

**Example Configuration:**

```csharp
builder.Services.AddElsa(elsa =>
{
    elsa.UseHttp(http =>
    {
        // Require HTTPS for all HTTP workflows
        http.RequireHttps = true;
        
        // Base URL for generated resume URLs
        http.BaseUrl = new Uri("https://elsa.example.com");
    });
});
```

## Studio-Specific Notes

### Embedding Studio Behind Ingress

When deploying Elsa Studio (the visual workflow designer) in a clustered environment:

**Deployment Pattern:**

```
                    ┌─────────────────┐
                    │  Ingress/LB     │
                    └────┬───────┬────┘
                         │       │
             ┌───────────┘       └───────────┐
             ↓                               ↓
    ┌─────────────────┐           ┌─────────────────┐
    │  Elsa Server    │           │  Elsa Studio    │
    │  (API)          │           │  (UI)           │
    │  Replicas: 3+   │←──────────│  Replicas: 2+   │
    └─────────────────┘           └─────────────────┘
```

**Configuration Example:**

```yaml
# Ingress routing
spec:
  rules:
    - host: elsa.example.com
      http:
        paths:
          # Studio UI
          - path: /
            pathType: Prefix
            backend:
              service:
                name: elsa-studio
                port:
                  number: 80
          # API
          - path: /elsa/api
            pathType: Prefix
            backend:
              service:
                name: elsa-server
                port:
                  number: 80
```

### Session Affinity for Studio UI

**Do you need sticky sessions for Studio?**

**Short answer: No** - If Studio is a stateless SPA (Single Page Application) that only communicates with the API.

**Long answer:**

* Elsa Studio (Blazor WebAssembly) is stateless and doesn't require session affinity
* All state is managed by the Elsa Server API (which uses distributed state)
* Studio can be freely routed to any pod

**Exception:** If using Elsa Studio Blazor Server (not WebAssembly), you **do** need session affinity:

```yaml
annotations:
  nginx.ingress.kubernetes.io/affinity: "cookie"
  nginx.ingress.kubernetes.io/session-cookie-name: "elsa-studio-session"
  nginx.ingress.kubernetes.io/session-cookie-max-age: "3600"
```

**Recommendation:** Use Elsa Studio WebAssembly for clustered deployments to avoid session affinity complexity.

### Ingress Settings

**Recommended Annotations (NGINX Ingress):**

```yaml
metadata:
  annotations:
    # SSL redirect
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    
    # Body size (for uploading large workflow definitions)
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
    
    # Timeouts (for long-running workflow executions)
    nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
    
    # CORS (if Studio and API on different domains)
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/cors-allow-origin: "https://studio.example.com"
    nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, PUT, DELETE, OPTIONS"
    
    # No session affinity (not needed for Elsa Server or Studio WebAssembly)
    nginx.ingress.kubernetes.io/affinity: "none"
    
    # Rate limiting (optional, for API protection)
    nginx.ingress.kubernetes.io/limit-rps: "100"
```

### Studio Authentication in Clusters

**Scenario:** Multiple Studio pods behind a load balancer.

**Requirements:**

1. **Shared authentication provider** (don't use in-memory auth)
2. **Distributed session storage** (if using cookie-based auth)
3. **Token-based authentication** (recommended for stateless clusters)

**Example: JWT Bearer Token Authentication:**

```csharp
// In Elsa Server (API)
builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowsApi(api =>
    {
        api.AddJwtAuthentication(jwt =>
        {
            jwt.Authority = "https://identity.example.com";
            jwt.Audience = "elsa-api";
        });
    });
});

// In Elsa Studio, use the 3.7.0 Blazor host pattern from
// guides/studio/integration/README.md and set:
// Backend:Url = "https://api.example.com/elsa/api"
// Authentication:Provider = "OpenIdConnect"
```

**Alternative: OpenID Connect with External Provider:**

* Azure AD
* Auth0
* Keycloak
* IdentityServer

This ensures authentication state is managed externally and works seamlessly across all cluster nodes.

## Placeholders for Screenshots

*\[Screenshot: Elsa Studio showing workflow definitions synchronized across nodes]*

*\[Screenshot: Kubernetes dashboard displaying 3 healthy Elsa pods with auto-scaling enabled]*

*\[Screenshot: Grafana dashboard showing distributed lock acquisition metrics and Quartz job execution rates]*

*\[Screenshot: Logs demonstrating only one node executing a scheduled timer across a 3-node cluster]*

*\[Screenshot: Redis Commander showing distributed lock keys with TTL]*

*\[Screenshot: PostgreSQL query result showing Quartz cluster state with multiple scheduler instances]*

## Related Documentation

* [Distributed Hosting](/hosting/distributed-hosting) - Core distributed runtime concepts
* [Kubernetes Deployment Guide](/guides/kubernetes-deployment) - General Kubernetes deployment
* [Database Configuration](/getting-started/database-configuration) - Database setup
* [Authentication Guide](/guides/authentication) - Securing your deployment

## Example Code Repository

For complete, deployable examples:

* [elsa-samples](https://github.com/elsa-workflows/elsa-samples) - Official sample projects
* [elsa-guides](https://github.com/elsa-workflows/elsa-guides) - Step-by-step guide implementations

## References

* Elsa Core Source Code: <https://github.com/elsa-workflows/elsa-core>
* Medallion.Threading: <https://github.com/madelson/DistributedLock>
* Quartz.NET: <https://www.quartz-scheduler.net/>
* MassTransit: <https://masstransit.io/>

## Support

* [GitHub Discussions](https://github.com/elsa-workflows/elsa-core/discussions)
* [GitHub Issues](https://github.com/elsa-workflows/elsa-core/issues)
* Community: Discord, Slack (see main README for links)

***

**Last Updated:** 2025-11-24


# Performance & Scaling

Release-backed guidance for measuring and tuning Elsa 3.8.0 throughput without trading away workflow durability or operational visibility by accident.

Tune Elsa from measurements, not from a generic worker-count target. A useful baseline includes representative workflow definitions, production-like persistence, and traffic that contains both new starts and resume work. Record throughput, end-to-end latency, database pressure, and the rate of incidents before changing one setting at a time.

In `release/3.8.0`, the main controls are:

1. how often workflow state is committed;
2. how much mediator work the host processes concurrently;
3. whether in-workflow dispatch uses the transactional outbox; and
4. the persistence, logging, and trace data retained for each execution.

## Start with the bottleneck

Use the workflow and activity spans from `Elsa.Workflows` to distinguish slow activity work from dispatcher, persistence, or downstream-service pressure. The built-in meter publishes `elsa.workflow.started`, `elsa.workflow.completed`, `elsa.workflow.faulted`, and `elsa.activity.duration`. The accompanying spans include workflow and activity identifiers, status, correlation ID, and tenant ID. See [Distributed Tracing](/operate/distributed-tracing) for exporter setup.

| Observation                                                  | Investigate before increasing concurrency                                 |
| ------------------------------------------------------------ | ------------------------------------------------------------------------- |
| Activity durations rise while arrivals stay steady           | the activity's downstream dependency, connection pool, or resource limit  |
| Workflow duration rises around commits                       | database latency, state size, bookmarks, variables, and log persistence   |
| Work arrives faster than it completes                        | command, job, or notification worker saturation; then downstream capacity |
| Cross-workflow dispatch is slow or unreliable after a commit | transactional-outbox settings and the outbox store                        |

Do not treat a faster benchmark as sufficient. Re-run failure and restart cases after every tuning change: a commit policy defines the recovery boundary.

## Choose a commit policy deliberately

A commit persists more than the workflow row. Elsa's default commit handler persists bookmark changes, activity execution logs, workflow execution logs, variables, and workflow state, then runs deferred work. More commits therefore usually improve durability and state visibility while adding persistence work.

`UseCommitStrategies` registers strategies that a workflow definition can select. Registering a strategy does not make it the default; set a fallback explicitly when definitions without a selection need one.

```csharp
using Elsa.Extensions;
using Elsa.Workflows.CommitStates;
using Elsa.Workflows.CommitStates.Strategies;

builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflows(workflows =>
    {
        workflows.UseCommitStrategies(strategies =>
        {
            strategies.AddStandardStrategies();
            strategies.Add(
                "Periodic10Seconds",
                "Every 10 seconds",
                "Commit workflow state at least every 10 seconds during execution.",
                new PeriodicWorkflowStrategy(TimeSpan.FromSeconds(10)));
        });

        // Fallback for definitions with no CommitStrategyName.
        workflows.WithDefaultWorkflowCommitStrategy(
            new PeriodicWorkflowStrategy(TimeSpan.FromSeconds(10)));
    });
});
```

`AddStandardStrategies()` registers these workflow strategy names:

| Name                | When it commits                | Typical fit                                           |
| ------------------- | ------------------------------ | ----------------------------------------------------- |
| `WorkflowExecuting` | when workflow execution starts | capture initial state early                           |
| `WorkflowExecuted`  | when workflow execution ends   | short workflows where reduced write volume matters    |
| `ActivityExecuting` | before each activity executes  | higher durability and diagnostic visibility           |
| `ActivityExecuted`  | after each activity executes   | workflows needing state after each completed activity |

`PeriodicWorkflowStrategy` starts with a commit, then commits when its interval has elapsed. Give it a stable registry name and a meaningful display name with the four-argument `Add(...)` overload. `CommitStrategyName` stores the stable name (`Periodic10Seconds` in the example), while Studio shows the display name. This also avoids overwriting one periodic interval with another. It is a good starting point for long-running workflows only after you test its recovery behavior and database cost under realistic load.

### Select the policy per workflow

Set `WorkflowOptions.CommitStrategyName` to the registered name. In Elsa Studio, open the workflow's **Properties**, choose **Settings**, and select **Commit Strategy**. Studio loads the available strategies from the backend and saves the selected name in the workflow definition; the empty **Default** selection uses the host fallback.

Use a per-workflow policy when one workload needs durability after every step while another is short-lived and can safely reduce persistence churn. Avoid inventing method calls such as `UseWorkflowExecutedStrategy()` or `UsePeriodicStrategy()`—they are not part of the 3.8.0 API.

## Increase worker counts carefully

Elsa's mediator has independent command, notification, and job worker counts, each defaulting to four. Change only the queue that matches measured backlog; raising all three multiplies concurrent work against the same database and external systems.

```csharp
using Elsa.Mediator.Options;

builder.Services.Configure<MediatorOptions>(options =>
{
    options.CommandWorkerCount = 8;
    options.JobWorkerCount = 4;
    options.NotificationWorkerCount = 4;
});
```

The background workflow dispatcher queues work through the command path and returns before that work is executed. Start with a small increase, watch activity duration, database saturation, and faults, then keep or revert it. More workers are not a substitute for a slow activity implementation or an undersized downstream service.

## Decide whether dispatch needs an outbox

For dispatch initiated during workflow execution, `WorkflowDispatcherOptions` can enable a transactional outbox. With `UseTransactionalOutbox` enabled, Elsa writes eligible dispatches with the workflow state commit and delivers them afterward. This makes recovery behavior more robust, but adds persistence and delivery work to the path. See the [Workflow dispatch outbox](/guides/architecture/workflow-dispatch-outbox) for the delivery lifecycle, retry limits, orphan cleanup, and duplicate- delivery caveats.

```csharp
using Elsa.Workflows.Runtime.Options;

builder.Services.Configure<WorkflowDispatcherOptions>(options =>
{
    options.UseTransactionalOutbox = true;
    options.ProcessOutboxAfterCommit = true;
    options.OutboxProcessorBatchSize = 100;
});
```

`ProcessOutboxAfterCommit` defaults to `true`: disable it only when you accept waiting for the recurring outbox sweep in exchange for lower commit-path work. Tune `OutboxProcessorBatchSize` from observed backlog and database capacity; do not increase it blindly.

## A safe tuning loop

1. Establish a baseline with representative starts, resumes, faults, and restarts.
2. Identify one bottleneck in traces, metrics, and persistence telemetry.
3. Change one commit policy, worker count, or outbox setting.
4. Re-run the same load and recovery test; compare throughput, latency, database pressure, and incident rate.
5. Keep the change only when the whole operating profile improves.

For distributed topology and locking prerequisites, see [Distributed Hosting](/hosting/distributed-hosting). For retaining less execution-log data, see [Log Persistence](/optimize/log-persistence) and make that a separate measurement-driven decision.

## Related guides

* [Throughput tuning examples](/guides/performance/throughput-tuning)
* [Worker count](/optimize/workers)
* [Distributed tracing](/operate/distributed-tracing)
* [Source references](https://github.com/elsa-workflows/elsa-gitbook/tree/main/guides/performance/README-REFERENCES.md)


# Throughput Tuning

Small, release-backed Elsa 3.8.0 configuration examples for testing commit, worker, and transactional-outbox tradeoffs under load.

These are starting configurations, not production targets. Benchmark each change against your own workflow shape, persistence provider, and downstream services. Keep recovery tests in the benchmark: throughput is only useful when the workflow remains correct after a restart or fault.

## Register selectable commit strategies

This configuration registers the standard strategy names and a named periodic strategy. A workflow definition can select any registered name through `WorkflowOptions.CommitStrategyName` or the Elsa Studio **Properties → Settings → Commit Strategy** selector.

```csharp
using Elsa.Extensions;
using Elsa.Workflows.CommitStates;
using Elsa.Workflows.CommitStates.Strategies;

builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflows(workflows =>
    {
        workflows.UseCommitStrategies(strategies =>
        {
            strategies.AddStandardStrategies();
            strategies.Add(
                "Periodic30Seconds",
                "Every 30 seconds",
                "Commit workflow state at least every 30 seconds during execution.",
                new PeriodicWorkflowStrategy(TimeSpan.FromSeconds(30)));
        });
    });
});
```

Use `WorkflowExecuted` as a test candidate for short workflows only after verifying the state that must survive a failure before completion. Use `ActivityExecuted` when persistence after each completed activity is more important than the additional writes. Test a named periodic strategy for long-running workflows that need a bounded recovery interval. In this example, the stored strategy name is `Periodic30Seconds`; Studio shows `Every 30 seconds`.

## Set a host fallback

Definitions with no commit-strategy selection use the host fallback. This is not added to the Studio/API registry, so register a named strategy separately when authors should be able to choose it.

```csharp
using Elsa.Extensions;
using Elsa.Workflows.CommitStates.Strategies;

builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflows(workflows =>
    {
        workflows.WithDefaultWorkflowCommitStrategy(
            new PeriodicWorkflowStrategy(TimeSpan.FromSeconds(30)));
    });
});
```

## Increase only the measured worker queue

The three mediator queues default to four workers each. If command processing is the proven bottleneck, begin with a modest command-only change and compare the result with the baseline.

```csharp
using Elsa.Mediator.Options;

builder.Services.Configure<MediatorOptions>(options =>
{
    options.CommandWorkerCount = 8;
    options.JobWorkerCount = 4;
    options.NotificationWorkerCount = 4;
});
```

The command path is relevant to background workflow dispatch. Do not increase the other counts merely to match it: notifications and jobs can put additional load on the same persistence and external dependencies.

## Use the transactional outbox for in-workflow dispatch

Enable the outbox when dispatching another workflow must be coordinated with the current workflow's state commit. It is not a free throughput feature: it adds persisted outbox work, so measure both successful dispatch latency and recovery behavior.

```csharp
using Elsa.Workflows.Runtime.Options;

builder.Services.Configure<WorkflowDispatcherOptions>(options =>
{
    options.UseTransactionalOutbox = true;
    options.ProcessOutboxAfterCommit = true;
    options.OutboxProcessorBatchSize = 100;
});
```

If the immediate delivery work is itself a measurable commit-path bottleneck, test `ProcessOutboxAfterCommit = false`. Delivery will then rely on the recurring processor, so validate the resulting dispatch delay and restart behavior before adopting it.

## Compare results consistently

For every run, capture:

* completed workflows per interval and end-to-end latency;
* `elsa.workflow.started`, `elsa.workflow.completed`, and `elsa.workflow.faulted` rates;
* `elsa.activity.duration` percentiles and the slowest activity spans;
* database resource use and commit latency; and
* the result of a forced-restart recovery test.

Use the same traffic mix for each run. Change one variable, retain the result only if it improves the system rather than shifting pressure to the database or a downstream dependency.

See [Performance tuning](/guides/performance) for the decision guide and [Distributed tracing](/operate/distributed-tracing) for Elsa's OpenTelemetry instrumentation.


# API & Client

A release-3.8 reference for Elsa's management API, .NET client interfaces, endpoint permissions, and bookmark-resume callbacks.

Use Elsa's HTTP API to automate workflow delivery and operations outside Elsa Studio. In .NET applications, use the `Elsa.Api.Client` interfaces instead of constructing request URLs and JSON by hand.

This page is grounded in Elsa `release/3.8.0`. It is a map of the high-value surfaces, not a replacement for the OpenAPI document when your host exposes one.

## Choose the right surface

| Need                                                       | Use                      | Notes                                                                   |
| ---------------------------------------------------------- | ------------------------ | ----------------------------------------------------------------------- |
| Create, publish, import, export, or query workflow designs | Workflow definitions API | A definition is a versioned design, not a running workflow.             |
| Start work now or place it on the dispatcher queue         | Execute or dispatch API  | `execute` runs immediately; `dispatch` hands work to the runtime queue. |
| Find, inspect, cancel, export, or diagnose executions      | Workflow instances API   | Use the journal and execution-state endpoints for diagnosis.            |
| Discover activity metadata for a designer or integration   | Activity descriptors API | Returns the activity catalogue exposed by the server.                   |
| Continue a waiting workflow from an external callback      | Bookmark-resume API      | This route is anonymous by design; its token is the capability.         |

The API base path is host-configurable. The examples use `https://elsa.example/elsa/api`; replace that whole prefix with the API base URL for your deployment.

## Authentication and permissions

Management endpoints are protected by Elsa permission claims when API security is enabled. For example, listing workflow definitions requires `read:workflow-definitions`, while listing workflow instances requires `read:workflow-instances`. Give an integration only the permissions it needs; do not reuse a Studio administrator token for background services.

Configure authentication and claims before calling these endpoints. See [Authentication & Authorization](/guides/authentication) and [Direct OpenID Connect](/guides/authentication/direct-openid-connect). For a route map and least-privilege role templates, see [Elsa API Permissions](/guides/authentication/permissions).

The exception is `GET`/`POST /bookmarks/resume`. Elsa deliberately allows this route anonymously because the encrypted `t` token identifies the bookmark and instance. Treat a resume URL like a secret: send it only over HTTPS, do not log it, and use a bounded lifetime when generating it.

## .NET client setup

`Elsa.Api.Client` supplies Refit-based interfaces for the management API. The API-key helper configures the same default clients and their base address.

```csharp
using Elsa.Api.Client.Extensions;

builder.Services.AddDefaultApiClientsUsingApiKey(options =>
{
    options.BaseAddress = new Uri("https://elsa.example/elsa/api");
    options.ApiKey = builder.Configuration["Elsa:ApiKey"]!;
});
```

For bearer tokens or another authentication scheme, use `AddDefaultApiClients` and configure the underlying `HttpClient` rather than adding an API key. Keep the base address at the API root: client routes start with paths such as `/workflow-definitions` and `/workflow-instances`.

Inject the narrowest interface that matches the job:

```csharp
using Elsa.Api.Client.Resources.WorkflowDefinitions.Contracts;
using Elsa.Api.Client.Resources.WorkflowDefinitions.Requests;

public sealed class WorkflowCatalog(IWorkflowDefinitionsApi definitions)
{
    public async Task ListAsync(CancellationToken cancellationToken)
    {
        await definitions.ListAsync(
            new ListWorkflowDefinitionsRequest { Page = 0, PageSize = 25 },
            cancellationToken: cancellationToken);
    }
}
```

## Core management API map

| Area                          | HTTP method and route (relative to the API base)            | .NET client interface                            | Typical permission             |
| ----------------------------- | ----------------------------------------------------------- | ------------------------------------------------ | ------------------------------ |
| List definitions              | `GET /workflow-definitions`                                 | `IWorkflowDefinitionsApi.ListAsync`              | `read:workflow-definitions`    |
| Get a definition by stable ID | `GET /workflow-definitions/by-definition-id/{definitionId}` | `IWorkflowDefinitionsApi.GetByDefinitionIdAsync` | `read:workflow-definitions`    |
| Save a definition             | `POST /workflow-definitions`                                | `IWorkflowDefinitionsApi.SaveAsync`              | `write:workflow-definitions`   |
| Publish a definition          | `POST /workflow-definitions/{definitionId}/publish`         | `IWorkflowDefinitionsApi.PublishAsync`           | `publish:workflow-definitions` |
| Run a definition now          | `POST /workflow-definitions/{definitionId}/execute`         | `IExecuteWorkflowApi.ExecuteAsync`               | `exec:workflow-definitions`    |
| Queue a definition to run     | `POST /workflow-definitions/{definitionId}/dispatch`        | `IExecuteWorkflowApi.DispatchAsync`              | `exec:workflow-definitions`    |
| List instances                | `GET` or `POST /workflow-instances`                         | `IWorkflowInstancesApi.ListAsync`                | `read:workflow-instances`      |
| Get an instance               | `GET /workflow-instances/{id}`                              | `IWorkflowInstancesApi.GetAsync`                 | `read:workflow-instances`      |
| Read an instance journal      | `GET /workflow-instances/{id}/journal`                      | `IWorkflowInstancesApi.GetJournalAsync`          | `read:workflow-instances`      |
| Read filtered journal records | `POST /workflow-instances/{id}/journal`                     | `IWorkflowInstancesApi.GetFilteredJournalAsync`  | `read:workflow-instances`      |
| Read execution state          | `GET /workflow-instances/{id}/execution-state`              | `IWorkflowInstancesApi.GetExecutionStateAsync`   | `read:workflow-instances`      |
| Cancel an instance            | `POST /cancel/workflow-instances/{id}`                      | `IWorkflowInstancesApi.CancelAsync`              | `cancel:workflow-instances`    |
| List activity descriptors     | `GET /descriptors/activities`                               | `IActivityDescriptorsApi.ListAsync`              | `read:activity-descriptors`    |

The table lists the principal routes rather than every bulk, import, export, reload, and administration endpoint. When your host exposes OpenAPI, use the document from the exact Elsa version you run for full schemas, optional fields, and the complete route set.

## Execute versus dispatch

Both operations create work from a workflow definition, but they have different operational intent:

* Use `execute` when the caller expects Elsa to start the workflow immediately.
* Use `dispatch` when the caller should hand off the request and let the configured workflow runtime consume it asynchronously.

Both client methods return `HttpResponseMessage`; inspect the status code and response before assuming an instance was created. Send input and correlation data through `ExecuteWorkflowDefinitionRequest` or `DispatchWorkflowDefinitionRequest`.

```csharp
using Elsa.Api.Client.Resources.WorkflowDefinitions.Contracts;
using Elsa.Api.Client.Resources.WorkflowDefinitions.Requests;

public sealed class OrderWorkflowStarter(IExecuteWorkflowApi workflows)
{
    public async Task StartAsync(string orderId, CancellationToken cancellationToken)
    {
        var response = await workflows.DispatchAsync(
            "process-order",
            new DispatchWorkflowDefinitionRequest
            {
                CorrelationId = orderId,
                Input = new Dictionary<string, object> { ["OrderId"] = orderId }
            },
            cancellationToken);

        response.EnsureSuccessStatusCode();
    }
}
```

## Inspect a running or failed workflow

Start with the instance list and filter by definition ID, correlation ID, status, sub-status, incident presence, or timestamps. Then choose the detail view that answers the question:

* **Instance**: current status and high-level instance data.
* **Journal**: chronological execution records; use the filtered journal for focused activity or incident investigation.
* **Execution state**: the persisted execution state for deeper runtime diagnosis.
* **Export**: a portable diagnostic or migration artifact.

Elsa Studio's workflow-instance viewer uses the same instance and journal surfaces. Prefer Studio for an interactive investigation; use the API for an operations integration, report, or controlled automation. For the operating workflow, see [Troubleshooting](/guides/troubleshooting) and [Long-Running Workflows](/guides/running-workflows/long-running-workflows).

## Resume a bookmark callback

An activity can generate a bookmark token URL for an approval or external callback. Resume it with the `t` query value and, optionally, input for the workflow. `POST` accepts an object containing `input`; `GET` accepts JSON in the `in` query parameter. The request body is limited to 1 MiB in Elsa 3.8.

```bash
curl -X POST "https://elsa.example/elsa/api/bookmarks/resume?t=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"input":{"Decision":"Approved"}}'
```

Add `async=true` to enqueue the resume request through Elsa's bookmark queue; without it, Elsa calls the workflow resumer directly. A successful accepted request returns `200 OK`. An invalid or undecryptable token returns a validation error.

Do not invent a bookmark token from an instance ID. Generate the token from the activity's bookmark, and make handlers idempotent because network callers can retry a callback.

## Before you automate

* Verify the host's API base path and authentication scheme.
* Give the client a least-privilege permission set.
* Use definition IDs for stable automation; versions and internal IDs change as designs are published.
* Persist the instance ID and correlation ID returned or supplied by your integration so operators can find the execution later.
* Prefer `dispatch` for fire-and-forget integrations and inspect the response for either path.
* Protect bookmark tokens as credentials, including in logs, browser history, support tickets, and analytics.

For release-specific request and response schemas, use the interfaces in the matching `Elsa.Api.Client` package and, when available, the OpenAPI document served by your host.


# Workflow-definition labels

Release-backed guide to organizing workflow definitions with labels in Elsa 3.8.0, including Studio setup, API usage, filtering, persistence, and permissions.

Labels are optional metadata for organizing workflow definitions. Use them for business or operational categories such as `Orders`, `Human review`, or `Production`. They are not workflow variables, activity labels, or runtime instance state.

In Elsa 3.8.0, the feature has two parts:

* Core stores labels and the associations between labels and workflow definition versions.
* Studio provides a Labels administration page and a label editor in the workflow-definition properties surface.

The feature is opt-in in a custom host. A project reference alone does not register the Core or Studio module.

## When to use labels

Use labels when people need to find or group definitions without changing how a workflow executes. They work well for:

* business ownership, such as `Finance` or `Customer support`;
* lifecycle or review state, such as `Draft review` or `Approved`;
* operational classification, such as `High priority` or `Nightly`.

Use workflow inputs, variables, or instance metadata when the value belongs to an execution. A label does not automatically appear on every instance created from a definition and does not change triggers, activities, or execution behavior.

## Enable the Core feature

Register the Labels feature in the Elsa host that serves the API. For a programmatic EF Core host, the release exposes this shape:

```csharp
using Elsa.Extensions;
using Elsa.Persistence.EFCore.Modules.Labels;

builder.Services.AddElsa(elsa =>
{
    elsa.UseLabels(labels => labels.UseEntityFrameworkCore());
});
```

`UseLabels` uses in-memory stores by default. That is useful for a test or short-lived process, but it is not durable across restarts. For production, select a persistence provider explicitly:

```csharp
using Elsa.Persistence.MongoDb.Modules.Labels;

builder.Services.AddElsa(elsa =>
{
    elsa.UseLabels(labels => labels.UseMongoDb());
});
```

The EF Core provider uses a separate `LabelsElsaDbContext` containing the `Labels` and `WorkflowDefinitionLabels` sets. Use the provider's released migrations or your normal Elsa migration process for that context. MongoDB stores the corresponding `labels` and `workflow_definition_labels` collections and creates label-association indexes.

The provider packages must match the rest of the Elsa release. The MongoDB extension is implemented in [`Elsa.Persistence.MongoDb.Modules.Labels`](https://github.com/elsa-workflows/elsa-extensions/tree/release/3.8.0/src/modules/persistence/Elsa.Persistence.MongoDb/Modules/Labels).

## Enable the Studio module

Register the Studio module in the host that serves the Studio UI:

```csharp
using Elsa.Studio.Labels;

builder.Services.AddLabelsModule(backendApiConfig);
```

With the module enabled, Studio adds a **Labels** item under **Administration** and a label editor to workflow-definition properties. From the Labels page, users can create, edit, and delete labels. From a workflow definition, users can add labels through the selection dialog or remove them from the displayed chips.

The module calls the backend through the currently configured `IBackendApiClientProvider`. If the module is missing, the menu and editor are not registered. If the backend feature or its permissions are missing, the Studio calls fail or the feature cannot load its data.

## Manage labels in Studio

1. Create a label from **Administration → Labels**.
2. Give it a required name. Optionally set a description and color.
3. Open a workflow definition in the designer.
4. In the workflow-definition properties, choose **Add Label**.
5. Select one or more existing labels and confirm.
6. Remove a label from the properties surface by closing its chip.

The Studio editor associates labels with the definition version currently being edited. Publishing a definition does not turn labels into execution data; use the label for cataloging and filtering definitions.

## Use the HTTP API

The Core feature exposes these endpoints. The exact API prefix depends on the host's route configuration.

Label catalog operations:

* `GET /labels` and `GET /labels/{id}` — `read:labels`.
* `POST /labels` — `create:labels`.
* `POST /labels/{id}` — `update:labels`.
* `DELETE /labels/{id}` — `delete:labels`.

Definition-association operations:

* `GET /workflow-definitions/{id}/labels` — `read:workflow-definition-labels`.
* `POST /workflow-definitions/{id}/labels` — `update:workflow-definition-labels`.

Create a label with a name, description, and optional color:

```http
POST /labels
Content-Type: application/json

{
  "name": "Human review",
  "description": "Definitions that require an approval step",
  "color": "#7E57C2"
}
```

The assignment endpoint takes the workflow definition version ID in the route and replaces the selected association set:

```http
POST /workflow-definitions/my-definition-version/labels
Content-Type: application/json

{
  "id": "my-definition-version",
  "labelIds": ["label-1", "label-2"]
}
```

The server keeps only label IDs that exist, calculates the difference from the current associations, and returns the label IDs that remain assigned. Sending an empty `labelIds` collection removes all labels from that definition version.

## Filter workflow definitions

The workflow-definition list request accepts a repeatable `label` query parameter. Use a label ID to filter definitions:

```http
GET /workflow-definitions?label=label-1
```

For multiple values, send the parameter repeatedly according to the API client's query-string conventions:

```http
GET /workflow-definitions?label=label-1&label=label-2
```

This filters the definition catalog. It does not filter workflow instances and does not add labels to a running workflow.

## Permissions and tenants

Grant only the operations each role needs. A catalog viewer generally needs `read:labels` and, when loading associations, `read:workflow-definition-labels`. Label administrators need the create, update, and delete permissions as well as the association update permission.

Labels and workflow-definition label associations inherit Elsa's common entity model, including `TenantId`. The EF Core mapping also indexes the association tenant ID. In a multi-tenant host, send requests through the same tenant resolution path used by the rest of the Elsa API and test that labels cannot cross tenant boundaries. Do not treat the label ID alone as a tenant-isolation mechanism.

## Troubleshooting

* **The Labels menu is missing:** register `AddLabelsModule` in the Studio host. A project reference does not register the module.
* **The editor cannot load labels:** verify that Core has `UseLabels`, the labels persistence store is configured, the backend URL is correct, and the caller has both label-read permissions.
* **Labels disappear after restart:** the host is using the default in-memory stores; configure EF Core or MongoDB persistence.
* **A definition list is empty:** confirm the query uses label IDs, not label names, and that the request is scoped to the intended tenant.
* **An assignment silently omits a label:** the update endpoint ignores IDs that do not resolve to existing labels. Create the label first or inspect the returned `labelIds` collection.

## Release source checked

This guide was checked against `release/3.8.0` at Core `5429008d98a`, Studio `d25f0aae`, and Extensions `335a2649`:

* [Core Labels feature](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/src/modules/Elsa.Labels/Features/LabelsFeature.cs)
* [Core label endpoints](https://github.com/elsa-workflows/elsa-core/tree/release/3.8.0/src/modules/Elsa.Labels/Endpoints)
* [Core definition list label filter](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/List/Models.cs)
* [Core EF Core label persistence](https://github.com/elsa-workflows/elsa-core/tree/release/3.8.0/src/modules/Elsa.Persistence.EFCore/Modules/Labels)
* [Studio Labels module](https://github.com/elsa-workflows/elsa-studio/tree/release/3.8.0/src/modules/Elsa.Studio.Labels)
* [Extensions MongoDB label persistence](https://github.com/elsa-workflows/elsa-extensions/tree/release/3.8.0/src/modules/persistence/Elsa.Persistence.MongoDb/Modules/Labels)

## Related guides

* [API Reference](/guides/api-client)
* [Authentication and permissions](/guides/authentication/permissions)
* [Multitenancy](/multitenancy/introduction)
* [Studio integration](/guides/studio/integration)


# Persistence

Comprehensive guide to choosing, configuring, and tuning persistence providers for Elsa Workflows v3, covering EF Core, MongoDB, Dapper, and Elasticsearch, along with retention, migrations, and operat

## Executive Summary

Elsa Workflows uses persistence providers to store workflow definitions, workflow instances, bookmarks, and execution logs. Choosing the right persistence strategy is critical for performance, scalability, and operational requirements. This guide covers:

* **Provider selection** — When to choose EF Core, MongoDB, Dapper, or Elasticsearch
* **Configuration patterns** — Connection strings, migrations, and store registration
* **Indexing recommendations** — Essential indexes for common queries
* **Retention & cleanup** — Managing completed workflows and bookmark cleanup
* **Migrations & versioning** — Handling schema changes and rolling upgrades
* **Observability** — Measuring persistence latency and tracing

## Persistence Stores Overview

Elsa organizes persistence into logical stores, each responsible for a specific data type:

| Store                            | Purpose                                         | Typical Table/Collection      |
| -------------------------------- | ----------------------------------------------- | ----------------------------- |
| **Workflow Definition Store**    | Stores published and draft workflow definitions | `WorkflowDefinitions`         |
| **Workflow Instance Store**      | Stores workflow execution state and history     | `WorkflowInstances`           |
| **Bookmark Store**               | Stores suspension points for workflow resume    | `Bookmarks`                   |
| **Activity Execution Store**     | Stores activity execution records               | `ActivityExecutionRecords`    |
| **Workflow Execution Log Store** | Stores detailed execution logs                  | `WorkflowExecutionLogRecords` |
| **Workflow Inbox Store**         | Stores incoming messages for correlation        | `WorkflowInboxMessages`       |

**Code Reference:** `src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs` — Registers workflow definition and instance stores.

**Code Reference:** `src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs` — Registers runtime stores (bookmarks, inbox, execution logs).

## Persistence Providers

Elsa supports several persistence providers. The provider is selected per store, so a deployment can combine providers when a specific workload needs it.

### Entity Framework Core (EF Core)

**Best for:** General-purpose relational database persistence with migration support.

**Supported Databases:**

* SQL Server
* PostgreSQL
* SQLite
* MySQL/MariaDB

**Pros:**

* ✅ Built-in migration support for schema versioning
* ✅ Mature ecosystem with robust tooling
* ✅ Transactional consistency across stores
* ✅ Wide database support

**Cons:**

* ❌ May have higher overhead for extremely high-throughput scenarios
* ❌ Requires migration management for schema changes

**When to Choose:**

* Production deployments requiring schema versioning
* Teams familiar with EF Core and relational databases
* Scenarios requiring transactional consistency

**Documentation:**

* [SQL Server Guide](/guides/persistence/sql-server) - Comprehensive SQL Server setup and configuration
* [EF Core Migrations Guide](/guides/persistence/ef-migrations) - Working with migrations and custom entities
* [EF Core Setup Example](/guides/persistence/efcore-setup) - Basic configuration patterns

### MongoDB

**Best for:** Document-oriented persistence with flexible schemas.

**Pros:**

* ✅ Flexible schema evolution without migrations
* ✅ Native document storage suits workflow state
* ✅ Horizontal scaling via sharding
* ✅ Built-in replication for high availability

**Cons:**

* ❌ No built-in migration tooling (schema changes require application logic)
* ❌ Custom indexes beyond Elsa's defaults must be managed manually
* ❌ Different consistency model than relational databases

**When to Choose:**

* Teams already using MongoDB
* Scenarios requiring flexible schema evolution
* High-volume workloads with horizontal scaling needs

See [MongoDB Setup Example](/guides/persistence/mongodb-setup) for configuration details.

### Dapper

**Best for:** Performance-critical scenarios requiring fine-grained SQL control.

**Pros:**

* ✅ Minimal ORM overhead
* ✅ Direct SQL control for optimization
* ✅ Lower memory footprint

**Cons:**

* ❌ Requires the Dapper migrations feature or external schema management
* ❌ Requires SQL expertise for customization
* ❌ Less abstraction than EF Core

**When to Choose:**

* Extreme performance requirements
* Teams with strong SQL expertise
* Scenarios requiring custom query optimization

See [Dapper Setup Example](/guides/persistence/dapper-setup) for configuration details.

### Elasticsearch

**Best for:** Deployments that already operate Elasticsearch and want workflow-instance and execution-log stores backed by Elasticsearch.

**Important boundary:** The 3.8.0 extension does not replace every Elsa store. It wires `IWorkflowInstanceStore` and `IWorkflowExecutionLogStore`; configure workflow definitions, bookmarks, inbox messages, and other stores separately. The release store also has filter and timestamp-update limitations, so validate your operational queries before choosing it as the primary persistence path.

See [Elasticsearch Setup Example](/guides/persistence/elasticsearch-setup) for the registration, index, authentication, and deployment guidance.

## Configuration Patterns

### Basic Configuration

All persistence providers are configured through the `services.AddElsa(...)` method:

```csharp
using Elsa.Extensions;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddElsa(elsa =>
{
    // Configure workflow management (definitions, instances)
    elsa.UseWorkflowManagement(management =>
    {
        management.UseEntityFrameworkCore(ef =>
        {
            ef.UsePostgreSql(builder.Configuration.GetConnectionString("PostgreSql"));
        });
    });
    
    // Configure workflow runtime (bookmarks, inbox, execution logs)
    elsa.UseWorkflowRuntime(runtime =>
    {
        runtime.UseEntityFrameworkCore(ef =>
        {
            ef.UsePostgreSql(builder.Configuration.GetConnectionString("PostgreSql"));
        });
    });
    
    // Enable API endpoints
    elsa.UseWorkflowsApi();
});

var app = builder.Build();
app.Run();
```

**Code Reference:** `src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs` — Core services wire-up.

### Connection Strings

**appsettings.json:**

```json
{
  "ConnectionStrings": {
    "PostgreSql": "Host=localhost;Database=elsa;Username=elsa;Password=YOUR_PASSWORD;Port=5432",
    "SqlServer": "Server=localhost;Database=Elsa;User Id=sa;Password=YOUR_PASSWORD;TrustServerCertificate=true",
    "MongoDb": "mongodb://localhost:27017/elsa"
  }
}
```

**Environment Variables:**

```bash
CONNECTIONSTRINGS__POSTGRESQL="Host=localhost;Database=elsa;..."
CONNECTIONSTRINGS__MONGODB="mongodb://localhost:27017/elsa"
```

### EF Core Migrations

For EF Core providers, migrations manage schema changes:

**1. Install EF Core Tools:**

```bash
dotnet tool install --global dotnet-ef
```

**2. Apply Migrations at Startup (Recommended for Development):**

```csharp
elsa.UseWorkflowManagement(management =>
{
    management.UseEntityFrameworkCore(ef =>
    {
        ef.UsePostgreSql(connectionString);
        ef.RunMigrations = true;  // Apply migrations on startup
    });
});
```

**3. Apply Migrations via CLI (Recommended for Production):**

```bash
# Generate migrations
dotnet ef migrations add InitialCreate --context ManagementElsaDbContext

# Apply migrations
dotnet ef database update --context ManagementElsaDbContext
```

**Schema Versioning Notes:**

* Always test migrations in a non-production environment first
* Use a staging database identical to production for migration testing
* Consider blue-green deployments for zero-downtime migrations
* Keep migration scripts in source control

For detailed information on working with EF Core migrations, adding custom entities, and migration strategies, see the [EF Core Migrations Guide](/guides/persistence/ef-migrations).

### MongoDB Configuration

MongoDB does not use migrations. Configure the shared MongoDB connection at the Elsa module level, then select MongoDB for the management and runtime stores:

```csharp
var connectionString = builder.Configuration.GetConnectionString("MongoDb")!;

builder.Services.AddElsa(elsa =>
{
    elsa.UseMongoDb(connectionString);

    elsa.UseWorkflowManagement(management =>
    {
        management.UseMongoDb();
    });

    elsa.UseWorkflowRuntime(runtime =>
    {
        runtime.UseMongoDb();
    });
});
```

The database name comes from the MongoDB connection string. Elsa creates its MongoDB indexes on startup and uses snake\_case collection names such as `workflow_definitions`, `workflow_instances`, `bookmarks`, `workflow_execution_logs`, and `activity_execution_logs`.

**Custom Indexes:** Create any additional workload-specific indexes yourself. See [Indexing Notes](/guides/persistence/indexing-notes) for examples and refer to [MongoDB Index Documentation](https://www.mongodb.com/docs/manual/indexes/) for detailed guidance.

**Mapping Considerations:**

* Elsa uses MongoDB driver's conventions for BSON serialization
* Custom activity data must be serializable to BSON
* Consider using `BsonIgnore` attribute for non-persisted properties

### Dapper Configuration

Dapper requires module-level connection provider configuration. Select the Dapper stores separately for workflow management and runtime:

```csharp
using Elsa.Persistence.Dapper.Extensions;
using Elsa.Persistence.Dapper.Services;

var connectionString = builder.Configuration.GetConnectionString("PostgreSql")!;

builder.Services.AddElsa(elsa =>
{
    elsa.UseDapper(dapper =>
    {
        dapper.DbConnectionProvider = _ => new PostgreSqlDbConnectionProvider(connectionString);
        dapper.UseMigrations();
    });

    elsa.UseWorkflowManagement(management => management.UseDapper());
    elsa.UseWorkflowRuntime(runtime => runtime.UseDapper());
});
```

**Schema Responsibility:**

* Use `dapper.UseMigrations()` to run Elsa's Dapper migrations for supported databases
* If you do not enable Elsa migrations, you are responsible for creating and maintaining the database schema
* Elsa's Dapper migrations create PascalCase tables and columns such as `WorkflowInstances`, `Bookmarks`, and `WorkflowExecutionLogRecords`
* See [Dapper Setup Example](/guides/persistence/dapper-setup) for a complete setup

## Indexes & Queries

Proper indexing is essential for production performance. Create indexes for frequently queried columns:

### Recommended Indexes

**Workflow Instances:**

```sql
-- Query by instance ID (primary key in most providers)
-- Query by correlation ID
CREATE INDEX idx_workflow_instances_correlation_id ON "WorkflowInstances"("CorrelationId");

-- Query by status (running, suspended, completed, faulted)
CREATE INDEX idx_workflow_instances_status ON "WorkflowInstances"("Status");

-- Query by definition ID
CREATE INDEX idx_workflow_instances_definition_id ON "WorkflowInstances"("DefinitionId");

-- Query by updated timestamp (for retention/cleanup)
CREATE INDEX idx_workflow_instances_updated_at ON "WorkflowInstances"("UpdatedAt");

-- Composite index for common queries
CREATE INDEX idx_workflow_instances_status_definition ON "WorkflowInstances"("Status", "DefinitionId");
```

**Bookmarks:**

```sql
-- Query by activity type + stimulus hash (primary lookup path)
CREATE INDEX idx_bookmarks_activity_type_hash ON "Bookmarks"("ActivityTypeName", "Hash");

-- Query by workflow instance ID (for cleanup)
CREATE INDEX idx_bookmarks_workflow_instance_id ON "Bookmarks"("WorkflowInstanceId");

-- Query by correlation ID
CREATE INDEX idx_bookmarks_correlation_id ON "Bookmarks"("CorrelationId");
```

**Incidents:**

```sql
-- Query by workflow instance ID
CREATE INDEX idx_incidents_workflow_instance_id ON "Incidents"("WorkflowInstanceId");

-- Query by timestamp (for monitoring dashboards)
CREATE INDEX idx_incidents_timestamp ON "Incidents"("Timestamp");
```

**Code Reference:** `src/modules/Elsa.Workflows.Core/Bookmarks/*` — Bookmark hashing and storage logic.

See [Indexing Notes](/guides/persistence/indexing-notes) for provider-specific guidance.

> **Note:** Defer detailed vendor-specific index tuning (covering indexes, partial indexes, index-only scans) to official database documentation.

## Retention & Cleanup

Over time, completed workflow instances and bookmarks accumulate. Configure retention policies to manage storage:

### Workflow Instance Retention

Use the built-in retention feature to automatically clean up old workflow instances:

```csharp
elsa.UseRetention(retention =>
{
    retention.SweepInterval = TimeSpan.FromHours(1);  // Check every hour
    
    // Delete completed workflows older than 30 days
    retention.AddDeletePolicy("Delete old completed workflows", sp =>
    {
        var clock = sp.GetRequiredService<ISystemClock>();
        var threshold = clock.UtcNow.AddDays(-30);
        
        return new RetentionWorkflowInstanceFilter
        {
            WorkflowStatus = WorkflowStatus.Finished,
            TimestampFilters = new[]
            {
                new TimestampFilter
                {
                    Column = nameof(WorkflowInstance.FinishedAt),
                    Operator = TimestampFilterOperator.LessThanOrEqual,
                    Timestamp = threshold
                }
            }
        };
    });
    
    // Delete faulted workflows older than 90 days
    retention.AddDeletePolicy("Delete old faulted workflows", sp =>
    {
        var clock = sp.GetRequiredService<ISystemClock>();
        var threshold = clock.UtcNow.AddDays(-90);
        
        return new RetentionWorkflowInstanceFilter
        {
            WorkflowStatus = WorkflowStatus.Faulted,
            TimestampFilters = new[]
            {
                new TimestampFilter
                {
                    Column = nameof(WorkflowInstance.FinishedAt),
                    Operator = TimestampFilterOperator.LessThanOrEqual,
                    Timestamp = threshold
                }
            }
        };
    });
});
```

**Code Reference:** `src/modules/Elsa.Workflows.Core/Models/WorkflowOptions.cs` — Retention context and options.

### Bookmark Cleanup

Orphaned bookmarks (where the associated workflow instance no longer exists) should be cleaned up:

```sql
-- Find orphaned bookmarks
SELECT b.* FROM "Bookmarks" b
LEFT JOIN "WorkflowInstances" wi ON b."WorkflowInstanceId" = wi."Id"
WHERE wi."Id" IS NULL;

-- Delete orphaned bookmarks
DELETE FROM "Bookmarks"
WHERE "WorkflowInstanceId" NOT IN (SELECT "Id" FROM "WorkflowInstances");
```

### Workflow Inbox Cleanup

The `WorkflowInboxCleanup` job removes stale inbox messages:

```csharp
elsa.UseWorkflowRuntime(runtime =>
{
    runtime.WorkflowInboxCleanupOptions = options =>
    {
        options.SweepInterval = TimeSpan.FromHours(1);
        options.Ttl = TimeSpan.FromDays(7);  // Remove messages older than 7 days
    };
});
```

**Code Reference:** `src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs` — Inbox cleanup options.

### Manual Cleanup (SQL)

For immediate cleanup needs:

```sql
-- Delete completed workflows older than 30 days
DELETE FROM "WorkflowInstances"
WHERE "Status" = 'Finished'
  AND "FinishedAt" < NOW() - INTERVAL '30 days';

-- Delete activity execution records for deleted instances
DELETE FROM "ActivityExecutionRecords"
WHERE "WorkflowInstanceId" NOT IN (SELECT "Id" FROM "WorkflowInstances");

-- Delete execution logs for deleted instances
DELETE FROM "WorkflowExecutionLogRecords"
WHERE "WorkflowInstanceId" NOT IN (SELECT "Id" FROM "WorkflowInstances");
```

## Backup & Restore

### Environment Consistency

When backing up and restoring Elsa databases:

1. **Version Alignment:** Ensure the Elsa version in your application matches the schema version in the database. Mismatched versions can cause runtime errors.
2. **Consistent Backups:** For clustered deployments, quiesce the cluster or use database-native snapshot capabilities to ensure consistency.
3. **Include All Stores:** If using separate databases for management and runtime stores, back up both.
4. **Test Restores:** Regularly test restore procedures in a non-production environment.

### Backup Commands

**PostgreSQL:**

```bash
# Full backup
pg_dump -h localhost -U elsa -d elsa -F c -f elsa_backup.dump

# Restore
pg_restore -h localhost -U elsa -d elsa elsa_backup.dump
```

**SQL Server:**

```sql
BACKUP DATABASE [Elsa] TO DISK = 'C:\Backups\Elsa.bak';

RESTORE DATABASE [Elsa] FROM DISK = 'C:\Backups\Elsa.bak';
```

**MongoDB:**

```bash
# Backup
mongodump --uri="mongodb://localhost:27017/elsa" --out=/backup/elsa

# Restore
mongorestore --uri="mongodb://localhost:27017/elsa" /backup/elsa
```

## Migrations & Versioning

### Managing Breaking Changes

When Elsa releases a new version with schema changes:

1. **Review Release Notes:** Check for migration steps or breaking changes.
2. **Test in Staging:** Apply migrations to a staging environment first.
3. **Rolling Upgrades:** For clustered deployments:
   * Apply database migrations first (backward-compatible changes)
   * Roll out new application version to nodes one at a time
   * Monitor for errors during transition
4. **Rollback Plan:** Keep database backups and have a rollback strategy.

### EF Core Migration Steps

**1. Update Elsa Packages:**

```bash
dotnet add package Elsa --version 3.x.x
dotnet add package Elsa.Persistence.EFCore.PostgreSql --version 3.x.x
```

**2. Generate Migration:**

```bash
dotnet ef migrations add UpdateToVersion3xx --context ManagementElsaDbContext
```

**3. Review Migration:** Inspect the generated migration file for potentially destructive changes.

**4. Apply Migration:**

```bash
# Development
dotnet ef database update --context ManagementElsaDbContext

# Production (generate SQL script for review)
dotnet ef migrations script --context ManagementElsaDbContext --idempotent
```

### Schema Versioning Best Practices

* Keep migrations in source control alongside application code
* Use semantic versioning to correlate Elsa versions with schema versions
* Document any manual data transformations required between versions
* Consider database branching strategies for team development

## Observability & Performance

### Measuring Persistence Latency

Monitor database operations to identify bottlenecks:

```csharp
using OpenTelemetry.Trace;

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        tracing
            .AddAspNetCoreInstrumentation()
            .AddNpgsql()  // PostgreSQL instrumentation
            .AddSource("Elsa.Workflows")
            .AddOtlpExporter();
    });
```

### Key Metrics to Monitor

| Metric                                 | Description                     | Alert Threshold   |
| -------------------------------------- | ------------------------------- | ----------------- |
| `db.query.duration`                    | Database query execution time   | P95 > 500ms       |
| `elsa.workflow_instance.save.duration` | Workflow state persistence time | P95 > 1000ms      |
| `elsa.bookmark.lookup.duration`        | Bookmark query time             | P95 > 100ms       |
| `db.connection.pool.active`            | Active database connections     | > 80% of max pool |

### Tracing and telemetry

For distributed tracing of workflow execution including persistence operations:

```csharp
using Elsa.Workflows.Telemetry;

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        tracing.AddAspNetCoreInstrumentation();
        tracing.AddHttpClientInstrumentation();
        tracing.AddSource(WorkflowInstrumentation.ActivitySourceName);
        tracing.AddOtlpExporter();
    });
```

See [Performance & Scaling Guide](/guides/performance) and [Monitoring & Observability](/operate/monitoring-observability) for the release-backed observability setup.

> **Note:** Elsa provides built-in OpenTelemetry instrumentation through `Elsa.Workflows`. Custom metrics beyond the built-in counters and histograms are still user-defined.

## Common Pitfalls

### 1. Long Transactions

**Problem:** Workflows with many activities in a single burst can hold database locks for extended periods.

**Symptoms:**

* Lock wait timeouts
* Blocked queries
* Degraded throughput under load

**Mitigation:**

* Use commit strategies to limit transaction scope (see [Performance Guide](/guides/performance))
* Configure shorter lock timeouts
* Consider breaking large workflows into smaller sub-workflows

### 2. High-Cardinality Bookmarks

**Problem:** Workflows creating many unique bookmarks (e.g., one per user or order) can overwhelm the bookmark index.

**Symptoms:**

* Slow bookmark lookups
* Index bloat
* Memory pressure

**Mitigation:**

* Limit bookmark cardinality by design
* Use correlation IDs to group related bookmarks
* Implement bookmark cleanup policies

**Code Reference:** `src/modules/Elsa.Workflows.Core/Bookmarks/*` — Understand bookmark hashing to design efficient bookmark strategies.

### 3. Missing Indexes

**Problem:** Production deployments without proper indexes suffer degraded query performance.

**Symptoms:**

* Full table scans in query plans
* Slow workflow list/search operations
* High database CPU

**Mitigation:**

* Apply recommended indexes (see [Indexing Notes](/guides/persistence/indexing-notes))
* Monitor slow query logs
* Use database-native query analysis tools

### 4. Noisy Logging of Large Payloads

**Problem:** Logging workflow inputs/outputs can expose sensitive data and bloat logs.

**Symptoms:**

* Excessive log volume
* Sensitive data in logs
* Log aggregation costs

**Mitigation:**

* Configure log levels appropriately for production
* Use structured logging with field exclusions
* Consider log retention policies

```json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Elsa": "Warning",
      "Elsa.Workflows.Runtime": "Information"
    }
  }
}
```

### 5. Connection Pool Exhaustion

**Problem:** High-concurrency workflows exhaust database connection pools.

**Symptoms:**

* Timeout waiting for connection
* Intermittent failures under load
* Degraded throughput

**Mitigation:**

* Increase connection pool size appropriately
* Monitor pool utilization metrics
* Configure connection timeout and retry policies

```csharp
// PostgreSQL example with pool settings
var connectionString = "Host=localhost;Database=elsa;Username=elsa;Password=...;MaxPoolSize=100;MinPoolSize=10";
```

## Related Documentation

* [SQL Server Guide](/guides/persistence/sql-server) — Complete SQL Server configuration and troubleshooting
* [EF Core Migrations Guide](/guides/persistence/ef-migrations) — Working with migrations and custom entities
* [Clustering Guide](/guides/clustering) — Distributed deployment and distributed locking (DOC-015)
* [Troubleshooting Guide](/guides/troubleshooting) — Diagnosing common issues (DOC-017)
* [Performance & Scaling Guide](/guides/performance) — Commit strategies and observability (DOC-021)
* [Database Configuration](/getting-started/database-configuration) — Basic database setup
* [Retention](/optimize/retention) — Detailed retention configuration
* [Log Persistence](/optimize/log-persistence) — Activity log optimization

## Example Files

* [EF Core Setup Example](/guides/persistence/efcore-setup)
* [MongoDB Setup Example](/guides/persistence/mongodb-setup)
* [Dapper Setup Example](/guides/persistence/dapper-setup)
* [Indexing Notes](/guides/persistence/indexing-notes)
* [Source File References](https://github.com/elsa-workflows/elsa-gitbook/tree/main/guides/persistence/README-REFERENCES.md)

***

**Last Updated:** 2025-11-28


# SQL Server

Complete guide to configuring SQL Server as the persistence provider for Elsa Workflows v3, including setup, configuration, and migration guidance.

This guide explains how to configure Elsa Workflows to use SQL Server as the persistence provider instead of SQLite. SQL Server is recommended for production deployments, especially on Windows environments, and provides robust transactional consistency and enterprise-grade reliability.

## Overview

Elsa uses two main persistence modules that must be configured consistently:

* **Workflow Management** - Stores workflow definitions and workflow instances
* **Workflow Runtime** - Stores bookmarks, workflow inbox messages, and execution logs

Both modules support SQL Server through Entity Framework Core.

## Prerequisites

* .NET 8.0 or later
* SQL Server 2016 or later (Express, Standard, or Enterprise)
* Elsa v3.x packages
* SQL Server instance accessible from your application

## NuGet Packages

Install the required packages:

```bash
dotnet add package Elsa
dotnet add package Elsa.Persistence.EFCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
```

**Package Descriptions:**

* `Elsa` - Core Elsa Workflows library
* `Elsa.Persistence.EFCore.SqlServer` - SQL Server persistence provider for Elsa
* `Microsoft.EntityFrameworkCore.SqlServer` - Entity Framework Core SQL Server driver

## Configuration

### Basic Setup

Configure SQL Server persistence in your `Program.cs`:

```csharp
using Elsa.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Get connection string from configuration
var connectionString = builder.Configuration.GetConnectionString("Elsa")
    ?? throw new InvalidOperationException("Connection string 'Elsa' not found.");

builder.Services.AddElsa(elsa =>
{
    // Configure workflow management persistence (definitions, instances)
    elsa.UseWorkflowManagement(management =>
    {
        management.UseEntityFrameworkCore(ef =>
        {
            ef.UseSqlServer(connectionString);
            
            // Optional: Run migrations automatically on startup (development only)
            // ef.RunMigrations = true;
        });
    });
    
    // Configure workflow runtime persistence (bookmarks, inbox, execution logs)
    elsa.UseWorkflowRuntime(runtime =>
    {
        runtime.UseEntityFrameworkCore(ef =>
        {
            ef.UseSqlServer(connectionString);
            
            // Optional: Run migrations automatically on startup (development only)
            // ef.RunMigrations = true;
        });
    });
    
    // Enable HTTP activities (optional)
    elsa.UseHttp();
    
    // Enable scheduling activities (optional)
    elsa.UseScheduling();
    
    // Enable API endpoints
    elsa.UseWorkflowsApi();
});

var app = builder.Build();

// Map Elsa API endpoints
app.UseWorkflows();

app.Run();
```

### Connection String Configuration

Add your SQL Server connection string to `appsettings.json`:

```json
{
  "ConnectionStrings": {
    "Elsa": "Server=localhost;Database=Elsa;User Id=sa;Password=YourPassword123;Encrypt=true;MultipleActiveResultSets=true"
  }
}
```

**Connection String Parameters:**

| Parameter                  | Description                        | Example                                                    |
| -------------------------- | ---------------------------------- | ---------------------------------------------------------- |
| `Server`                   | SQL Server hostname or IP          | `localhost`, `sql.example.com`, `192.168.1.10`             |
| `Database`                 | Database name                      | `Elsa`, `ElsaWorkflows`                                    |
| `User Id`                  | SQL Server authentication username | `sa`, `elsa_user`                                          |
| `Password`                 | User password                      | `YourPassword123`                                          |
| `TrustServerCertificate`   | Accept self-signed certificates    | `true` (development), `false` (production with valid cert) |
| `MultipleActiveResultSets` | Enable MARS for complex queries    | `true`                                                     |
| `Integrated Security`      | Use Windows authentication         | `true` (alternative to User Id/Password)                   |

**Windows Authentication Example:**

```json
{
  "ConnectionStrings": {
    "Elsa": "Server=localhost;Database=Elsa;Integrated Security=true;TrustServerCertificate=true;MultipleActiveResultSets=true"
  }
}
```

**Connection Pooling (High Concurrency):**

```json
{
  "ConnectionStrings": {
    "Elsa": "Server=localhost;Database=Elsa;User Id=sa;Password=YourPassword123;TrustServerCertificate=true;MultipleActiveResultSets=true;Max Pool Size=100;Min Pool Size=10"
  }
}
```

### Environment Variables

For containerized deployments, use environment variables:

```bash
# Linux/Docker
export ConnectionStrings__Elsa="Server=sql-server;Database=Elsa;User Id=sa;Password=YourPassword123;Encrypt=true"

# Windows PowerShell
$env:ConnectionStrings__Elsa="Server=localhost;Database=Elsa;Integrated Security=true;Encrypt=true"

# Docker Compose
environment:
  - ConnectionStrings__Elsa=Server=sql-server;Database=Elsa;User Id=sa;Password=YourPassword123;Encrypt=true
```

## Database Migrations

Elsa ships with default Entity Framework Core migrations that create the necessary tables and schema. You have two options for applying migrations:

### Option 1: Automatic Migrations (Development)

Set `RunMigrations = true` in your configuration to apply migrations automatically on application startup:

```csharp
elsa.UseWorkflowManagement(management =>
{
    management.UseEntityFrameworkCore(ef =>
    {
        ef.UseSqlServer(connectionString);
        ef.RunMigrations = true;  // Apply migrations on startup
    });
});

elsa.UseWorkflowRuntime(runtime =>
{
    runtime.UseEntityFrameworkCore(ef =>
    {
        ef.UseSqlServer(connectionString);
        ef.RunMigrations = true;  // Apply migrations on startup
    });
});
```

> **⚠️ Warning:** Automatic migrations are convenient for development but **not recommended for production**. Use manual migration deployment in production environments for better control.

### Option 2: Manual Migrations (Production)

For production deployments, apply migrations manually using the EF Core CLI:

**1. Install EF Core Tools:**

```bash
dotnet tool install --global dotnet-ef
```

**2. Apply Migrations:**

```bash
# Apply Management context migrations (workflow definitions, instances)
dotnet ef database update --context ManagementElsaDbContext

# Apply Runtime context migrations (bookmarks, inbox, execution logs)
dotnet ef database update --context RuntimeElsaDbContext
```

**3. Generate SQL Scripts for Review (Recommended):**

```bash
# Generate idempotent SQL script for review before applying
dotnet ef migrations script --context ManagementElsaDbContext --idempotent -o management-migrations.sql
dotnet ef migrations script --context RuntimeElsaDbContext --idempotent -o runtime-migrations.sql

# Review the generated SQL files, then apply using SQL Server tools:
sqlcmd -S localhost -d Elsa -i management-migrations.sql
sqlcmd -S localhost -d Elsa -i runtime-migrations.sql
```

### Database Schema

Elsa creates the following tables in SQL Server:

**Management Tables:**

* `WorkflowDefinitions` - Published and draft workflow definitions
* `WorkflowInstances` - Workflow execution state and history

**Runtime Tables:**

* `Bookmarks` - Workflow suspension points for resumption
* `WorkflowInboxMessages` - Incoming messages for workflow correlation
* `ActivityExecutionRecords` - Activity execution records
* `WorkflowExecutionLogRecords` - Detailed execution logs

**Additional Tables:**

* `__EFMigrationsHistory` - EF Core migration tracking

See [EF Core Migrations Guide](/guides/persistence/ef-migrations) for information on customizing migrations and adding your own entities.

## Advanced Configuration

### Separate Databases

Use separate databases for management and runtime data:

```csharp
builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowManagement(management =>
    {
        management.UseEntityFrameworkCore(ef =>
        {
            ef.UseSqlServer("Server=localhost;Database=ElsaManagement;...");
        });
    });
    
    elsa.UseWorkflowRuntime(runtime =>
    {
        runtime.UseEntityFrameworkCore(ef =>
        {
            ef.UseSqlServer("Server=localhost;Database=ElsaRuntime;...");
        });
    });
});
```

**Benefits:**

* Scale management and runtime databases independently
* Isolate operational data from definition data
* Apply different backup/retention policies

### Connection Resilience

Configure retry logic for transient failures:

```csharp
elsa.UseWorkflowManagement(management =>
{
    management.UseEntityFrameworkCore(ef =>
    {
        ef.UseSqlServer(connectionString, sqlOptions =>
        {
            sqlOptions.EnableRetryOnFailure(
                maxRetryCount: 5,
                maxRetryDelay: TimeSpan.FromSeconds(30),
                errorNumbersToAdd: null);
            
            sqlOptions.CommandTimeout = 60; // 60 seconds
        });
    });
});
```

### Performance Tuning

**Connection Pool Settings:**

```csharp
using Microsoft.Data.SqlClient;

var connectionString = new SqlConnectionStringBuilder
{
    DataSource = "localhost",
    InitialCatalog = "Elsa",
    UserID = "sa",
    Password = "YourPassword123",
    TrustServerCertificate = true,
    MultipleActiveResultSets = true,
    MaxPoolSize = 100,
    MinPoolSize = 10,
    ConnectTimeout = 30,
    ApplicationName = "ElsaWorkflows"
}.ToString();
```

**Indexing:**

For optimal query performance, ensure proper indexes exist. See [Indexing Notes](/guides/persistence/indexing-notes) for recommended indexes.

## Migrating from SQLite

To migrate an existing Elsa installation from SQLite to SQL Server:

**1. Update packages:**

```bash
# Remove SQLite packages
dotnet remove package Elsa.Persistence.EFCore.Sqlite
dotnet remove package Microsoft.EntityFrameworkCore.Sqlite

# Add SQL Server packages
dotnet add package Elsa.Persistence.EFCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
```

**2. Update configuration in `Program.cs`:**

```csharp
// Before (SQLite):
// ef.UseSqlite(connectionString);

// After (SQL Server):
ef.UseSqlServer(connectionString);
```

**3. Update connection string in `appsettings.json`:**

```json
{
  "ConnectionStrings": {
    "Elsa": "Server=localhost;Database=Elsa;User Id=sa;Password=YourPassword123;TrustServerCertificate=true"
  }
}
```

**4. Apply migrations to create the SQL Server schema:**

```bash
dotnet ef database update --context ManagementElsaDbContext
dotnet ef database update --context RuntimeElsaDbContext
```

**5. Optionally migrate data:**

If you need to preserve existing workflow definitions and instances, you'll need to export data from SQLite and import to SQL Server. This typically involves:

* Export SQLite tables to CSV or JSON
* Transform data if needed
* Import into SQL Server using BULK INSERT or SQL Server Import Wizard

> **Note:** Data migration between providers is a custom process not provided by Elsa. Consider starting fresh in SQL Server for new deployments.

## Troubleshooting

### Common Issues

**Error: "Cannot open database"**

**Cause:** Database doesn't exist or user lacks permissions.

**Solution:**

* Verify the database exists: `SELECT name FROM sys.databases;`
* Create the database manually: `CREATE DATABASE Elsa;`
* Grant user permissions: `ALTER SERVER ROLE sysadmin ADD MEMBER [elsa_user];`

**Error: "Login failed for user"**

**Cause:** Incorrect credentials or authentication mode.

**Solution:**

* Verify username and password
* Check SQL Server authentication mode (mixed mode required for SQL authentication)
* For Windows authentication, ensure the application pool identity has access

**Error: "A network-related or instance-specific error occurred"**

**Cause:** Cannot connect to SQL Server.

**Solution:**

* Verify SQL Server is running
* Check firewall rules (default port 1433)
* Enable TCP/IP protocol in SQL Server Configuration Manager
* Verify server name/IP address

**Error: "The migration has already been applied to the database"**

**Cause:** Migrations already applied (informational, not an error).

**Solution:** No action needed. This indicates the database schema is current.

### Diagnostic Logging

Enable detailed EF Core logging to diagnose issues:

```json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.EntityFrameworkCore": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Warning"
    }
  }
}
```

## Production Considerations

### Security

* **Use strong passwords** - Minimum 12 characters with complexity
* **Enable SSL/TLS** - Use `Encrypt=true` instead of `TrustServerCertificate=true`
* **Principle of least privilege** - Grant only required permissions to the Elsa database user
* **Secure connection strings** - Use Azure Key Vault or environment variables, not source control
* **Enable auditing** - Configure SQL Server audit logs for compliance

### High Availability

* **SQL Server Always On** - Use availability groups for automatic failover
* **Backup strategy** - Implement regular full and differential backups
* **Point-in-time recovery** - Enable full recovery model for transaction log backups
* **Disaster recovery** - Test restore procedures regularly

### Monitoring

Monitor these key metrics:

* Database connection pool utilization
* Query execution times (P95, P99)
* Lock wait statistics
* Deadlock frequency
* Database size growth
* Transaction log size

### Performance

* **Regularly update statistics** - `UPDATE STATISTICS` for query optimization
* **Rebuild/reorganize indexes** - Maintain index fragmentation below 10%
* **Monitor blocking queries** - Use `sp_who2` or Extended Events
* **Configure memory settings** - Allocate appropriate min/max server memory
* **Enable query store** - For query performance history and analysis

## Related Documentation

* [Persistence Guide](/guides/persistence) - Overview and provider comparison
* [EF Core Migrations Guide](/guides/persistence/ef-migrations) - Custom migrations and strategies
* [EF Core Setup Example](/guides/persistence/efcore-setup) - General EF Core configuration
* [Indexing Notes](/guides/persistence/indexing-notes) - Recommended indexes
* [Database Configuration](/getting-started/database-configuration) - Basic database setup
* [Performance & Scaling Guide](/guides/performance) - Throughput optimization

## Next Steps

* Configure connection strings for your environment
* Apply migrations to create the database schema
* Review [indexing recommendations](/guides/persistence/indexing-notes)
* Implement backup and monitoring procedures
* Consider [custom migrations](/guides/persistence/ef-migrations) if adding your own entities

***

**Last Updated:** 2025-12-01

**Addresses Issues:** #2 (SQL Server instead of SQLite), #11 (configuring persistence providers)


# EF Core Migrations

Complete guide to working with Entity Framework Core migrations in Elsa Workflows v3, including custom migrations, DbContext management, and versioning strategies.

This guide explains how Elsa Workflows uses Entity Framework Core migrations and how to customize them for your needs. Whether you want to add your own entities to the Elsa database or maintain separate migration strategies, this guide covers the essential patterns.

## Overview

Elsa Workflows uses Entity Framework Core (EF Core) for relational database persistence and includes built-in migrations that manage the database schema. Understanding how these migrations work is essential when:

* Adding custom entities to the Elsa database
* Generating combined migrations for Elsa + your application
* Managing schema changes during upgrades
* Working with multiple databases or contexts

## Elsa's DbContext Architecture

Elsa uses two separate `DbContext` classes to organize persistence concerns:

### ManagementElsaDbContext

**Purpose:** Stores workflow definitions and instances

**Key Tables:**

* `WorkflowDefinitions` - Published and draft workflow definitions with version history
* `WorkflowInstances` - Active and historical workflow execution state

**Typical Usage:**

* Workflow Designer (Studio) reads/writes definitions
* Workflow Runtime creates and updates instances during execution

### RuntimeElsaDbContext

**Purpose:** Stores runtime operational data

**Key Tables:**

* `Bookmarks` - Workflow suspension points for event-driven resumption
* `WorkflowInboxMessages` - Incoming messages for workflow correlation
* `ActivityExecutionRecords` - Detailed activity execution history
* `WorkflowExecutionLogRecords` - Execution logs for debugging and auditing

**Typical Usage:**

* Bookmark resolution when external events trigger workflows
* Workflow inbox for asynchronous message handling
* Execution log queries for monitoring and troubleshooting

> **Note:** Both contexts can use the same physical database but maintain separate migration histories, or they can use separate databases for scaling and isolation.

## How Elsa Migrations Work

### Built-in Migrations

Elsa ships with complete migrations that create and manage the database schema across versions. These migrations are embedded in the Elsa NuGet packages (`Elsa.Persistence.EFCore.SqlServer`, `Elsa.Persistence.EFCore.PostgreSql`, etc.).

**Migration Naming Convention:**

* Migrations follow a timestamped pattern: `YYYYMMDDHHMMSS_DescriptionOfChange`
* Example: `20240315120000_InitialCreate`, `20240520093000_AddWorkflowInbox`

**Automatic Application:**

Elsa can apply migrations automatically on startup:

```csharp
elsa.UseWorkflowManagement(management =>
{
    management.UseEntityFrameworkCore(ef =>
    {
        ef.UseSqlServer(connectionString);
        ef.RunMigrations = true;  // Apply migrations on startup
    });
});
```

> **⚠️ Warning:** Automatic migrations (`RunMigrations = true`) are convenient for development but **not recommended for production**. Use controlled migration deployment in production environments.

### Manual Migration Application

For production deployments, apply migrations manually:

```bash
# Install EF Core CLI tools
dotnet tool install --global dotnet-ef

# Apply Management context migrations
dotnet ef database update --context ManagementElsaDbContext

# Apply Runtime context migrations
dotnet ef database update --context RuntimeElsaDbContext
```

### Migration History

EF Core tracks applied migrations in the `__EFMigrationsHistory` table:

```sql
SELECT MigrationId, ProductVersion 
FROM __EFMigrationsHistory 
ORDER BY MigrationId DESC;
```

This table ensures migrations are only applied once and enables EF Core to understand the current schema version.

## Adding Custom Entities to Elsa's Database

A common scenario is adding your own entities to the same database used by Elsa. This approach offers several benefits:

**Benefits:**

* Single database simplifies deployment and management
* Share transaction scope between Elsa and your entities
* Unified backup and recovery
* Simplified connection string management

**Trade-offs:**

* Couples your schema to Elsa's schema
* Requires careful migration management
* Elsa version upgrades may require migration coordination

### Strategy: Separate DbContext with Shared Database

The recommended approach is to create your own `DbContext` that references the same database but maintains independent migrations:

**1. Create Your DbContext:**

```csharp
using Microsoft.EntityFrameworkCore;

namespace MyApp.Data
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        // Your entities
        public DbSet<Order> Orders { get; set; }
        public DbSet<Customer> Customers { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            // Configure your entities
            modelBuilder.Entity<Order>(entity =>
            {
                entity.HasKey(e => e.Id);
                entity.Property(e => e.OrderNumber).IsRequired();
                entity.HasOne(e => e.Customer)
                    .WithMany(c => c.Orders)
                    .HasForeignKey(e => e.CustomerId);
            });

            modelBuilder.Entity<Customer>(entity =>
            {
                entity.HasKey(e => e.Id);
                entity.Property(e => e.Name).IsRequired();
            });
        }
    }

    public class Order
    {
        public string Id { get; set; } = default!;
        public string OrderNumber { get; set; } = default!;
        public string CustomerId { get; set; } = default!;
        public Customer Customer { get; set; } = default!;
        public DateTime CreatedAt { get; set; }
    }

    public class Customer
    {
        public string Id { get; set; } = default!;
        public string Name { get; set; } = default!;
        public List<Order> Orders { get; set; } = new();
    }
}
```

**2. Register Your DbContext in `Program.cs`:**

```csharp
using Elsa.Extensions;
using MyApp.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("Database");

// Register Elsa with its contexts
builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowManagement(management =>
    {
        management.UseEntityFrameworkCore(ef => ef.UseSqlServer(connectionString));
    });
    
    elsa.UseWorkflowRuntime(runtime =>
    {
        runtime.UseEntityFrameworkCore(ef => ef.UseSqlServer(connectionString));
    });
    
    elsa.UseWorkflowsApi();
});

// Register your own DbContext using the SAME connection string
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));

var app = builder.Build();
app.Run();
```

**3. Configure Design-Time DbContext Factory (Required for Migrations):**

Create a file `ApplicationDbContextFactory.cs` in your project root:

```csharp
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using MyApp.Data;

namespace MyApp
{
    public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
    {
        public ApplicationDbContext CreateDbContext(string[] args)
        {
            var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
            
            // Use a connection string for design-time operations
            // This is ONLY used by 'dotnet ef' commands, not runtime
            var connectionString = "Server=localhost;Database=Elsa;User Id=sa;Password=YourPassword123;Encrypt=true";
            optionsBuilder.UseSqlServer(connectionString);

            return new ApplicationDbContext(optionsBuilder.Options);
        }
    }
}
```

> **💡 Tip:** Alternatively, you can specify the connection string via the `--connection` parameter when running `dotnet ef` commands instead of hardcoding it in the factory.

**4. Generate Your Migrations:**

```bash
# Create initial migration for your entities
dotnet ef migrations add InitialCreate --context ApplicationDbContext

# Review the generated migration in Migrations/ folder

# Apply the migration
dotnet ef database update --context ApplicationDbContext
```

**5. Managing Updates:**

When you add or modify entities:

```bash
# Add new migration
dotnet ef migrations add AddOrderStatusColumn --context ApplicationDbContext

# Review the generated migration

# Apply to database
dotnet ef database update --context ApplicationDbContext
```

### Project Structure

A typical project structure with custom migrations:

```
MyElsaApp/
├── Program.cs
├── ApplicationDbContextFactory.cs
├── Data/
│   ├── ApplicationDbContext.cs
│   ├── Order.cs
│   └── Customer.cs
├── Migrations/                    # Your custom migrations
│   ├── 20250101120000_InitialCreate.cs
│   └── 20250115140000_AddOrderStatusColumn.cs
└── appsettings.json
```

Elsa's migrations remain in the Elsa NuGet packages and are applied separately.

## Migration Commands Reference

### Common EF Core CLI Commands

**Install/Update EF Tools:**

```bash
dotnet tool install --global dotnet-ef
dotnet tool update --global dotnet-ef
```

**Add a New Migration:**

```bash
dotnet ef migrations add <MigrationName> --context <ContextName>

# Examples:
dotnet ef migrations add AddCustomerTable --context ApplicationDbContext
dotnet ef migrations add InitialElsaSetup --context ManagementElsaDbContext
```

**Apply Migrations:**

```bash
# Update to latest migration
dotnet ef database update --context <ContextName>

# Update to specific migration
dotnet ef database update <MigrationName> --context <ContextName>

# Rollback to specific migration
dotnet ef database update <PreviousMigrationName> --context <ContextName>
```

**Generate SQL Scripts:**

```bash
# Generate idempotent script (safe to run multiple times)
dotnet ef migrations script --context <ContextName> --idempotent -o migrations.sql

# Generate script for specific migration range
dotnet ef migrations script <FromMigration> <ToMigration> --context <ContextName> -o update.sql
```

**List Migrations:**

```bash
dotnet ef migrations list --context <ContextName>
```

**Remove Last Migration (if not applied):**

```bash
dotnet ef migrations remove --context <ContextName>
```

**Drop Database (Caution!):**

```bash
dotnet ef database drop --context <ContextName>
```

### Specifying Connection Strings

**Via Command Line:**

```bash
dotnet ef database update --context ApplicationDbContext \
  --connection "Server=localhost;Database=Elsa;User Id=sa;Password=Pass123"
```

**Via Environment Variable:**

```bash
export ConnectionStrings__Database="Server=localhost;Database=Elsa;..."
dotnet ef database update --context ApplicationDbContext
```

### Working with Multiple Contexts

When managing both Elsa contexts and your own:

```bash
# Update all contexts in sequence
dotnet ef database update --context ManagementElsaDbContext
dotnet ef database update --context RuntimeElsaDbContext
dotnet ef database update --context ApplicationDbContext

# Or use a script to automate
#!/bin/bash
for context in ManagementElsaDbContext RuntimeElsaDbContext ApplicationDbContext; do
  echo "Updating $context..."
  dotnet ef database update --context $context
done
```

## Migration Strategies

### Strategy 1: Single Shared Database

**Description:** Elsa and your application share a single database with separate contexts and independent migrations.

**Configuration:**

```csharp
var connectionString = builder.Configuration.GetConnectionString("Database");

// All contexts use the same connection string
builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowManagement(m => m.UseEntityFrameworkCore(ef => ef.UseSqlServer(connectionString)));
    elsa.UseWorkflowRuntime(r => r.UseEntityFrameworkCore(ef => ef.UseSqlServer(connectionString)));
});

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
```

**Pros:**

* ✅ Simple deployment and connection management
* ✅ Share transaction scope between Elsa and app data
* ✅ Single backup/restore process

**Cons:**

* ❌ All schemas coupled in one database
* ❌ Difficult to scale components independently
* ❌ Schema changes impact all consumers

**Best For:** Small to medium applications, single-server deployments, development environments

### Strategy 2: Separate Databases

**Description:** Elsa uses one database, your application uses another.

**Configuration:**

```csharp
var elsaConnectionString = builder.Configuration.GetConnectionString("Elsa");
var appConnectionString = builder.Configuration.GetConnectionString("Application");

builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowManagement(m => m.UseEntityFrameworkCore(ef => ef.UseSqlServer(elsaConnectionString)));
    elsa.UseWorkflowRuntime(r => r.UseEntityFrameworkCore(ef => ef.UseSqlServer(elsaConnectionString)));
});

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(appConnectionString));
```

**Pros:**

* ✅ Clear separation of concerns
* ✅ Independent scaling of Elsa and app databases
* ✅ Different backup/retention policies
* ✅ Easier to upgrade Elsa without impacting app schema

**Cons:**

* ❌ No shared transactions across databases
* ❌ More complex connection management
* ❌ Two backup/restore processes

**Best For:** Large applications, microservices architectures, scenarios requiring independent scaling

### Strategy 3: Split Elsa Management and Runtime

**Description:** Separate databases for Elsa's management and runtime contexts.

**Configuration:**

```csharp
var managementConnectionString = builder.Configuration.GetConnectionString("ElsaManagement");
var runtimeConnectionString = builder.Configuration.GetConnectionString("ElsaRuntime");

builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowManagement(m => 
        m.UseEntityFrameworkCore(ef => ef.UseSqlServer(managementConnectionString)));
    
    elsa.UseWorkflowRuntime(r => 
        r.UseEntityFrameworkCore(ef => ef.UseSqlServer(runtimeConnectionString)));
});
```

**Pros:**

* ✅ Scale management (definitions) separately from runtime (executions)
* ✅ Different retention policies (keep definitions longer, purge old executions)
* ✅ Isolate high-volume runtime data from stable definition data

**Cons:**

* ❌ More infrastructure to manage
* ❌ Additional connection configuration

**Best For:** High-throughput scenarios, compliance requirements, environments with different SLAs for definitions vs. execution data

## Handling Elsa Version Upgrades

### Review Release Notes

When upgrading Elsa to a new version:

1. **Check Release Notes** - Review migration changes in the [Elsa Core Release Notes](https://github.com/elsa-workflows/elsa-core/releases)
2. **Review Migration Files** - Examine new migrations in the updated NuGet packages
3. **Test in Staging** - Apply migrations in a non-production environment first
4. **Backup Before Upgrade** - Always backup databases before applying migrations

### Upgrade Process

**1. Update NuGet Packages:**

```bash
dotnet add package Elsa --version 3.x.x
dotnet add package Elsa.Persistence.EFCore.SqlServer --version 3.x.x
```

**2. Review Pending Migrations:**

```bash
# List migrations that will be applied
dotnet ef migrations list --context ManagementElsaDbContext
dotnet ef migrations list --context RuntimeElsaDbContext
```

**3. Generate SQL Scripts for Review:**

```bash
# Generate scripts to review changes before applying
dotnet ef migrations script --context ManagementElsaDbContext --idempotent -o elsa-management-upgrade.sql
dotnet ef migrations script --context RuntimeElsaDbContext --idempotent -o elsa-runtime-upgrade.sql
```

**4. Apply Migrations:**

```bash
# Apply in test environment first
dotnet ef database update --context ManagementElsaDbContext
dotnet ef database update --context RuntimeElsaDbContext

# Verify application starts and workflows execute correctly

# Then apply to production
```

**5. Rolling Upgrades (Clustered Environments):**

For zero-downtime upgrades:

1. Apply backward-compatible database migrations first
2. Deploy new application version to nodes one at a time
3. Monitor for errors during the transition
4. Keep previous version ready for rollback

### Rollback Strategy

If a migration causes issues:

**1. Rollback Database:**

```bash
# Rollback to specific migration
dotnet ef database update <PreviousMigrationName> --context ManagementElsaDbContext
```

**2. Restore from Backup:**

```bash
# SQL Server example
RESTORE DATABASE [Elsa] FROM DISK = 'C:\Backups\Elsa-PreUpgrade.bak'
```

**3. Revert Application Version:**

```bash
# Redeploy previous version
dotnet publish --configuration Release -o /path/to/previous/version
```

## Troubleshooting

### Common Issues

**Error: "The term 'dotnet-ef' is not recognized"**

**Cause:** EF Core tools not installed.

**Solution:**

```bash
dotnet tool install --global dotnet-ef
# Add ~/.dotnet/tools to PATH if needed
```

**Error: "Unable to create an object of type 'ApplicationDbContext'"**

**Cause:** Missing design-time DbContext factory or configuration.

**Solution:** Create a `IDesignTimeDbContextFactory<T>` implementation as shown above, or specify the connection string via command-line parameter.

**Error: "The migration has already been applied to the database"**

**Cause:** Migration already applied (informational).

**Solution:** No action needed. This is normal if the database is up to date.

**Error: "Cannot find compilation library location for package"**

**Cause:** Project not built before running `dotnet ef` commands.

**Solution:**

```bash
dotnet build
dotnet ef migrations add MigrationName
```

**Error: "Pending model changes detected"**

**Cause:** Entity model changes not captured in a migration.

**Solution:**

```bash
dotnet ef migrations add CaptureModelChanges --context ApplicationDbContext
```

### Diagnostic Commands

**Check Current Migration Status:**

```bash
# List migrations (applied migrations marked with *)
dotnet ef migrations list --context ApplicationDbContext

# Check database status
dotnet ef database update --context ApplicationDbContext --verbose
```

**View Last Migration Details:**

```sql
SELECT TOP 1 MigrationId, ProductVersion 
FROM __EFMigrationsHistory 
ORDER BY MigrationId DESC;
```

**Test Connection:**

```bash
# Attempt to connect and display info
dotnet ef dbcontext info --context ApplicationDbContext
```

## For Maintainers

### Elsa Core Issue Reference

This guidance addresses [elsa-core issue #6355](https://github.com/elsa-workflows/elsa-core/issues/6355), which requests clearer documentation on EF Core migration strategies and custom entity integration.

**Key Requirements from Issue:**

* ✅ Document Elsa's DbContext architecture
* ✅ Show how to add custom entities to Elsa's database
* ✅ Provide migration strategy guidance
* ✅ Explain manual vs. automatic migration approaches
* ✅ Cover version upgrade scenarios

### Schema Versioning Best Practices

For teams maintaining Elsa-based applications:

1. **Keep Migrations in Source Control** - Commit all migration files alongside application code
2. **Use Semantic Versioning** - Tag releases with versions that correspond to schema versions
3. **Document Schema Changes** - Maintain a CHANGELOG for notable schema modifications
4. **Test Migrations** - Include migration testing in CI/CD pipelines
5. **Database Branching** - Consider separate databases per branch for feature development

## Related Documentation

* [Persistence Guide](/guides/persistence) - Overview and provider comparison
* [SQL Server Guide](/guides/persistence/sql-server) - SQL Server-specific configuration
* [EF Core Setup Example](/guides/persistence/efcore-setup) - Basic EF Core configuration
* [Database Configuration](/getting-started/database-configuration) - Getting started with databases
* [Performance & Scaling Guide](/guides/performance) - Optimization strategies

## Next Steps

* Decide on a migration strategy (single vs. separate databases)
* Create your `DbContext` if adding custom entities
* Generate and review migrations before applying
* Implement backup procedures before schema changes
* Automate migration deployment in your CI/CD pipeline

***

**Last Updated:** 2025-12-01

**Addresses Issues:** #74 (generating custom EF Core migrations), #11 (persistence configuration), references elsa-core #6355


# EF Core Setup

Minimal example to enable Entity Framework Core persistence for Elsa Workflows, including database provider setup and migrations.

This document provides a minimal, copy-pasteable example for configuring Elsa Workflows with Entity Framework Core persistence.

## Prerequisites

* .NET 8.0 or later
* Database server (PostgreSQL, SQL Server, SQLite, or MySQL)
* Elsa v3.x packages

## NuGet Packages

**For PostgreSQL:**

```bash
dotnet add package Elsa
dotnet add package Elsa.Persistence.EFCore.PostgreSql
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
```

**For SQL Server:**

```bash
dotnet add package Elsa
dotnet add package Elsa.Persistence.EFCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
```

**For SQLite:**

```bash
dotnet add package Elsa
dotnet add package Elsa.Persistence.EFCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
```

## Minimal Configuration

### Program.cs

```csharp
using Elsa.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Get connection string from configuration
var connectionString = builder.Configuration.GetConnectionString("PostgreSql")
    ?? throw new InvalidOperationException("Connection string 'PostgreSql' not found.");

builder.Services.AddElsa(elsa =>
{
    // Configure workflow management (definitions, instances)
    elsa.UseWorkflowManagement(management =>
    {
        management.UseEntityFrameworkCore(ef =>
        {
            // Use PostgreSQL (replace with UseSqlServer(), UseSqlite(), etc. as needed)
            ef.UsePostgreSql(connectionString);
            
            // Apply migrations on startup (development only)
            ef.RunMigrations = true;
        });
    });
    
    // Configure workflow runtime (bookmarks, inbox, execution logs)
    elsa.UseWorkflowRuntime(runtime =>
    {
        runtime.UseEntityFrameworkCore(ef =>
        {
            ef.UsePostgreSql(connectionString);
            ef.RunMigrations = true;
        });
    });
    
    // Enable HTTP activities (optional)
    elsa.UseHttp();
    
    // Enable scheduling activities (optional)
    elsa.UseScheduling();
    
    // Enable API endpoints
    elsa.UseWorkflowsApi();
});

var app = builder.Build();

// Map Elsa API endpoints
app.UseWorkflows();

app.Run();
```

### appsettings.json

**PostgreSQL:**

```json
{
  "ConnectionStrings": {
    "PostgreSql": "Host=localhost;Database=elsa;Username=elsa;Password=YOUR_PASSWORD;Port=5432"
  }
}
```

**SQL Server:**

```json
{
  "ConnectionStrings": {
    "SqlServer": "Server=localhost;Database=Elsa;User Id=sa;Password=YOUR_PASSWORD;TrustServerCertificate=true"
  }
}
```

**SQLite:**

```json
{
  "ConnectionStrings": {
    "Sqlite": "Data Source=elsa.db"
  }
}
```

## Applying Migrations

### Option 1: Automatic Migrations (Development)

Set `ef.RunMigrations = true` in the configuration above. Migrations will be applied automatically when the application starts.

> **Warning:** Automatic migrations are convenient for development but not recommended for production. Use manual migrations in production environments.

### Option 2: CLI Migrations (Production)

**1. Install EF Core Tools:**

```bash
dotnet tool install --global dotnet-ef
```

**2. Generate Migration Script (for review):**

```bash
dotnet ef migrations script --context ManagementElsaDbContext --idempotent -o migrations.sql
```

**3. Apply Migrations:**

```bash
# Direct application
dotnet ef database update --context ManagementElsaDbContext

# Or apply the generated script via database tools
psql -h localhost -U elsa -d elsa -f migrations.sql
```

### Multiple Contexts

Elsa uses separate DbContexts for different concerns:

* `ManagementElsaDbContext` — Workflow definitions and instances
* `RuntimeElsaDbContext` — Bookmarks, inbox, execution logs

Apply migrations for both contexts:

```bash
dotnet ef database update --context ManagementElsaDbContext
dotnet ef database update --context RuntimeElsaDbContext
```

## Advanced Configuration

### Separate Databases

Use separate databases for management and runtime data:

```csharp
builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowManagement(management =>
    {
        management.UseEntityFrameworkCore(ef =>
        {
            ef.UsePostgreSql(managementConnectionString);
        });
    });
    
    elsa.UseWorkflowRuntime(runtime =>
    {
        runtime.UseEntityFrameworkCore(ef =>
        {
            ef.UsePostgreSql(runtimeConnectionString);
        });
    });
});
```

### Connection Pooling

Configure connection pool settings for high-concurrency scenarios:

```csharp
var connectionString = new NpgsqlConnectionStringBuilder
{
    Host = "localhost",
    Database = "elsa",
    Username = "elsa",
    Password = "YOUR_PASSWORD",
    MaxPoolSize = 100,        // Increase for high concurrency
    MinPoolSize = 10,         // Keep connections warm
    ConnectionIdleLifetime = 300,  // 5 minutes
    CommandTimeout = 60       // 1 minute
}.ToString();
```

### Retry on Transient Failures

Configure retry logic for transient database errors:

```csharp
elsa.UseWorkflowManagement(management =>
{
    management.UseEntityFrameworkCore(ef =>
    {
        ef.UsePostgreSql(connectionString, options =>
        {
            options.EnableRetryOnFailure(
                maxRetryCount: 5,
                maxRetryDelay: TimeSpan.FromSeconds(30),
                errorCodesToAdd: null);
        });
    });
});
```

## Troubleshooting

### Migration Errors

**Error:** `The term 'dotnet-ef' is not recognized`

**Solution:** Install EF Core tools:

```bash
dotnet tool install --global dotnet-ef
```

**Error:** `No migrations were applied. The database is already up to date.`

**Solution:** This is informational. The database schema is current.

**Error:** `Login failed for user` or `password authentication failed`

**Solution:** Verify connection string credentials and database permissions.

### Logging

Enable EF Core logging to diagnose issues:

```json
{
  "Logging": {
    "LogLevel": {
      "Microsoft.EntityFrameworkCore": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Warning"
    }
  }
}
```

## Related Documentation

* [Persistence Guide](/guides/persistence) — Overview and provider comparison
* [Indexing Notes](/guides/persistence/indexing-notes) — Recommended indexes for EF Core
* [Database Configuration](/getting-started/database-configuration) — Basic setup

***

**Last Updated:** 2025-11-28


# MongoDB Setup

Minimal example to enable MongoDB persistence for Elsa Workflows, including connection configuration and indexing notes.

This document provides a minimal, copy-pasteable example for configuring Elsa Workflows with MongoDB persistence.

## Prerequisites

* .NET 8.0 or later
* MongoDB 4.4 or later
* Elsa v3.x packages

## NuGet Packages

```bash
dotnet add package Elsa
dotnet add package Elsa.Persistence.MongoDb
```

## Minimal Configuration

### Program.cs

```csharp
using Elsa.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Get connection string from configuration
var mongoConnectionString = builder.Configuration.GetConnectionString("MongoDb")
    ?? throw new InvalidOperationException("Connection string 'MongoDb' not found.");

builder.Services.AddElsa(elsa =>
{
    // Configure the shared MongoDB connection.
    elsa.UseMongoDb(mongoConnectionString);

    // Configure workflow management (definitions, instances)
    elsa.UseWorkflowManagement(management =>
    {
        management.UseMongoDb();
    });
    
    // Configure workflow runtime (bookmarks, inbox, execution logs)
    elsa.UseWorkflowRuntime(runtime =>
    {
        runtime.UseMongoDb();
    });
    
    // Enable HTTP activities (optional)
    elsa.UseHttp();
    
    // Enable scheduling activities (optional)
    elsa.UseScheduling();
    
    // Enable API endpoints
    elsa.UseWorkflowsApi();
});

var app = builder.Build();

// Map Elsa API endpoints
app.UseWorkflows();

app.Run();
```

### appsettings.json

```json
{
  "ConnectionStrings": {
    "MongoDb": "mongodb://localhost:27017/elsa"
  }
}
```

**With Authentication:**

```json
{
  "ConnectionStrings": {
    "MongoDb": "mongodb://username:password@localhost:27017/elsa?authSource=admin"
  }
}
```

**Replica Set:**

```json
{
  "ConnectionStrings": {
    "MongoDb": "mongodb://node1:27017,node2:27017,node3:27017/elsa?replicaSet=rs0"
  }
}
```

## Index Creation

Elsa creates the MongoDB indexes it needs on startup. You do not need to run a separate index creation script for the built-in workflow management and runtime stores.

Elsa uses snake\_case collection names, including:

* `workflow_definitions`
* `workflow_instances`
* `triggers`
* `bookmarks`
* `bookmark_queue_items`
* `workflow_execution_logs`
* `activity_execution_logs`
* `key_value_pairs`

Use MongoDB shell commands only for verification or for additional workload-specific indexes:

```javascript
use elsa;

db.workflow_definitions.getIndexes();
db.workflow_instances.getIndexes();
db.bookmarks.getIndexes();
db.workflow_execution_logs.getIndexes();
```

## Advanced Configuration

### Custom Database and Collection Names

```csharp
var mongoConnectionString = "mongodb://localhost:27017/my_workflows";

builder.Services.AddElsa(elsa =>
{
    elsa.UseMongoDb(mongoConnectionString);

    elsa.UseWorkflowManagement(management =>
    {
        management.UseMongoDb();
    });
});
```

The database name is read from the connection string. For custom collection names, replace the `CollectionNamingStrategy` on the MongoDB feature.

### Connection Pool Settings

Configure MongoDB driver settings such as pool sizes in the connection string:

```json
{
  "ConnectionStrings": {
    "MongoDb": "mongodb://localhost:27017/elsa?maxPoolSize=100&minPoolSize=10&waitQueueTimeoutMS=30000&connectTimeoutMS=10000"
  }
}
```

### Read Preference for Replicas

For read-heavy workloads with replica sets:

```csharp
using MongoDB.Driver;

elsa.UseMongoDb(mongoConnectionString, options =>
{
    options.ReadPreference = ReadPreference.SecondaryPreferred;
});
```

## Mapping Considerations

### Custom Activity Data

When storing custom data in activities, ensure it's BSON-serializable:

```csharp
// Good: Simple types serialize automatically
public class MyActivityData
{
    public string Name { get; set; }
    public int Count { get; set; }
    public DateTime Timestamp { get; set; }
}

// For complex types, ensure serialization works
public class ComplexData
{
    public Dictionary<string, object> Properties { get; set; }
    
    [BsonIgnore]  // Exclude from persistence
    public Func<Task> Callback { get; set; }
}
```

### BSON Serialization Settings

Elsa uses MongoDB driver conventions. For custom serialization needs:

```csharp
// Register custom serializers before AddElsa
BsonSerializer.RegisterSerializer(new GuidSerializer(GuidRepresentation.Standard));

// Or use convention packs
var pack = new ConventionPack
{
    new CamelCaseElementNameConvention(),
    new IgnoreExtraElementsConvention(true)
};
ConventionRegistry.Register("ElsaConventions", pack, t => true);
```

## TTL Collections

MongoDB supports TTL (Time-To-Live) indexes for automatic document expiration:

```javascript
// Auto-delete workflow execution logs after 30 days
db.workflow_execution_logs.createIndex(
    { "Timestamp": 1 },
    { expireAfterSeconds: 2592000 }  // 30 days
);

// Auto-delete completed workflow instances after 60 days
// Note: Only works if you have a dedicated TTL field
db.workflow_instances.createIndex(
    { "ExpiresAt": 1 },
    { expireAfterSeconds: 0 }  // Expire at the ExpiresAt time
);
```

## Troubleshooting

### Connection Issues

**Error:** `Unable to connect to server`

**Solutions:**

* Verify MongoDB is running: `mongosh --eval "db.runCommand({ping: 1})"`
* Check connection string format
* Verify network connectivity and firewall rules
* For replica sets, ensure all nodes are accessible

**Error:** `Authentication failed`

**Solutions:**

* Verify username/password
* Check `authSource` parameter in connection string
* Ensure user has appropriate roles: `readWrite` on the elsa database

### Performance Issues

**Slow queries:**

1. Verify indexes are created: `db.collection.getIndexes()`
2. Use explain plans: `db.collection.find({...}).explain("executionStats")`
3. Check for collection scans in slow query logs

**High memory usage:**

* Review connection pool settings
* Consider adding a TTL index for log collections
* Implement retention policies for old workflow instances

### Logging

Enable MongoDB driver logging:

```json
{
  "Logging": {
    "LogLevel": {
      "MongoDB": "Debug"
    }
  }
}
```

## Related Documentation

* [Persistence Guide](/guides/persistence) — Overview and provider comparison
* [Indexing Notes](/guides/persistence/indexing-notes) — Detailed indexing guidance
* [MongoDB Documentation](https://www.mongodb.com/docs/manual/) — Official MongoDB docs

***

**Last Updated:** 2025-11-28


# Dapper Setup

Minimal example to enable Dapper persistence for Elsa Workflows, including connection provider setup and migrations.

This document provides a minimal, copy-pasteable example for configuring Elsa Workflows with Dapper persistence.

## Prerequisites

* .NET 8.0 or later
* Database server (PostgreSQL or SQL Server)
* Elsa v3.x packages
* Elsa Dapper migrations enabled, or schema managed externally

## NuGet Packages

**For PostgreSQL:**

```bash
dotnet add package Elsa
dotnet add package Elsa.Persistence.Dapper
dotnet add package Npgsql
```

**For SQL Server:**

```bash
dotnet add package Elsa
dotnet add package Elsa.Persistence.Dapper
dotnet add package Microsoft.Data.SqlClient
```

## When to Use Dapper

Dapper is ideal for:

* **Performance-critical scenarios** requiring minimal ORM overhead
* **Fine-grained SQL control** for custom query optimization
* **Existing database schemas** where you want to integrate Elsa
* **Teams with strong SQL expertise** who prefer direct control

Consider EF Core instead if you need:

* Higher-level abstractions
* Simpler configuration

## Minimal Configuration

### Program.cs

```csharp
using Elsa.Extensions;
using Elsa.Persistence.Dapper.Extensions;
using Elsa.Persistence.Dapper.Services;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("PostgreSql")
    ?? throw new InvalidOperationException("Connection string 'PostgreSql' not found.");

builder.Services.AddElsa(elsa =>
{
    // Configure the shared Dapper connection provider and migrations.
    elsa.UseDapper(dapper =>
    {
        dapper.DbConnectionProvider = _ => new PostgreSqlDbConnectionProvider(connectionString);
        dapper.UseMigrations();
    });
    
    // Configure workflow management with Dapper
    elsa.UseWorkflowManagement(management => management.UseDapper());

    // Configure workflow runtime with Dapper
    elsa.UseWorkflowRuntime(runtime => runtime.UseDapper());
    
    // Enable HTTP activities (optional)
    elsa.UseHttp();
    
    // Enable scheduling activities (optional)
    elsa.UseScheduling();
    
    // Enable API endpoints
    elsa.UseWorkflowsApi();
});

var app = builder.Build();

// Map Elsa API endpoints
app.UseWorkflows();

app.Run();
```

### SQL Server Example

```csharp
using Elsa.Persistence.Dapper.Extensions;
using Elsa.Persistence.Dapper.Services;

builder.Services.AddElsa(elsa =>
{
    elsa.UseDapper(dapper =>
    {
        dapper.DbConnectionProvider = _ => new SqlServerDbConnectionProvider(connectionString);
        dapper.UseMigrations();
    });

    elsa.UseWorkflowManagement(management => management.UseDapper());
    elsa.UseWorkflowRuntime(runtime => runtime.UseDapper());
});
```

### appsettings.json

**PostgreSQL:**

```json
{
  "ConnectionStrings": {
    "PostgreSql": "Host=localhost;Database=elsa;Username=elsa;Password=YOUR_PASSWORD;Port=5432"
  }
}
```

**SQL Server:**

```json
{
  "ConnectionStrings": {
    "SqlServer": "Server=localhost;Database=Elsa;User Id=sa;Password=YOUR_PASSWORD;TrustServerCertificate=true"
  }
}
```

## Schema Creation

Use `dapper.UseMigrations()` to let Elsa create and update the Dapper schema for supported databases. The migrations are included with `Elsa.Persistence.Dapper` and use PascalCase table and column names.

Core workflow tables created by the migrations include:

* `WorkflowDefinitions`
* `WorkflowInstances`
* `Triggers`
* `Bookmarks`
* `WorkflowExecutionLogRecords`
* `ActivityExecutionRecords`
* `WorkflowInboxMessages`

If you manage the schema externally, mirror the 3.7.0 migrations from `src/modules/persistence/Elsa.Persistence.Dapper.Migrations` and keep the PascalCase names. Do not use the snake\_case MongoDB collection names or older hand-written SQL snippets for Dapper.

## Transactions

Dapper operations participate in ambient transactions. For explicit control:

```csharp
using System.Transactions;

public class MyWorkflowService
{
    private readonly IWorkflowInstanceStore _store;
    
    public async Task PerformTransactionalOperation()
    {
        using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
        
        // Multiple operations in a single transaction
        await _store.SaveAsync(instance1);
        await _store.SaveAsync(instance2);
        
        scope.Complete();  // Commit
    }
}
```

## Performance Tuning

### Connection Pool Configuration

```csharp
// PostgreSQL with connection pool settings
var connectionString = new NpgsqlConnectionStringBuilder
{
    Host = "localhost",
    Database = "elsa",
    Username = "elsa",
    Password = "YOUR_PASSWORD",
    MaxPoolSize = 100,
    MinPoolSize = 10,
    ConnectionIdleLifetime = 300,
    CommandTimeout = 60
}.ToString();

dapper.DbConnectionProvider = _ => new PostgreSqlDbConnectionProvider(connectionString);
```

### Batch Operations

Dapper excels at batch operations with low overhead:

```csharp
// Example: Batch delete with Dapper
using var connection = new NpgsqlConnection(connectionString);
await connection.ExecuteAsync(
    @"DELETE FROM ""WorkflowInstances""
      WHERE ""Status"" = @Status
      AND ""FinishedAt"" < @Threshold",
    new { Status = "Finished", Threshold = DateTime.UtcNow.AddDays(-30) }
);
```

## Migration Strategy

For supported databases, prefer Elsa's Dapper migrations:

```csharp
elsa.UseDapper(dapper =>
{
    dapper.DbConnectionProvider = _ => new PostgreSqlDbConnectionProvider(connectionString);
    dapper.UseMigrations();
});
```

For production deployments, run the application once in a controlled deployment step or environment where migrations are allowed to apply. If your organization requires separately reviewed SQL scripts, generate or maintain those scripts from the 3.7.0 Dapper migration definitions and preserve the PascalCase table and column names.

## Troubleshooting

### Connection Issues

**Error:** `Connection refused` or `timeout`

**Solutions:**

* Verify database server is running
* Check connection string format
* Ensure network connectivity

### Schema Mismatch

**Error:** `relation "WorkflowInstances" does not exist`

**Solution:** Enable `dapper.UseMigrations()` or apply the equivalent 3.7.0 Dapper migration scripts before running the application.

### Performance Issues

**Slow queries:**

1. Verify indexes exist
2. Use database query analyzer (EXPLAIN ANALYZE in PostgreSQL)
3. Check connection pool metrics

## Related Documentation

* [Persistence Guide](/guides/persistence) — Overview and provider comparison
* [Indexing Notes](/guides/persistence/indexing-notes) — Detailed indexing guidance
* [EF Core Setup](/guides/persistence/efcore-setup) — Alternative with migration support

***

**Last Updated:** 2025-11-28


# Elasticsearch Setup

Configure the Elsa 3.8.0 Elasticsearch extension for workflow-instance and execution-log persistence, with its provider boundaries and production caveats.

Elsa 3.8.0 includes an `Elsa.Persistence.Elasticsearch` extension for teams that already operate Elasticsearch and want Elasticsearch-backed workflow instance and workflow execution-log stores. It is not a complete replacement for every Elsa persistence store: workflow definitions, bookmarks, inbox messages, activity execution records, and other stores remain backed by the provider you configure for them.

## When to use it

Choose Elasticsearch when:

* your operations team already runs Elasticsearch and can manage its index, mapping, security, and retention lifecycle;
* workflow-instance and execution-log search are important workloads; and
* you are comfortable validating the release implementation's filter and update limitations before using it for production operations.

Use [EF Core](/guides/persistence/efcore-setup), [MongoDB](/guides/persistence/mongodb-setup), or [Dapper](/guides/persistence/dapper-setup) when you need a provider guide for the broader Elsa management and runtime store set. A mixed setup is possible, but document the store ownership explicitly so operators know where each type of data lives.

## Packages and endpoint

Install the extension package that matches the rest of your Elsa 3.8.0 package set:

```bash
dotnet add package Elsa.Persistence.Elasticsearch --version 3.8.0
```

`ElasticsearchOptions.Endpoint` can be either an Elasticsearch URI or the name of a .NET connection string. The extension first looks up a connection string with that name and otherwise treats the value as the URI.

For example:

```json
{
  "ConnectionStrings": {
    "Elasticsearch": "https://elasticsearch.example.com:9200"
  },
  "Elasticsearch": {
    "ApiKey": "store-this-in-your-secret-provider"
  }
}
```

Prefer a secret manager, mounted configuration, or environment variables for credentials. The extension supports either an API key or a username/password pair. When both are supplied, the API key is selected.

## Configure the stores

The extension has three separate registration points:

* `elsa.UseElasticsearch(...)` registers the shared Elasticsearch client and connection options.
* `management.UseElasticsearch()` replaces the workflow-instance store.
* `runtime.UseElasticsearch()` replaces the workflow-execution-log store.

Enable only the stores you intend to move. The following example enables both Elasticsearch stores while leaving the other Elsa stores with their existing configuration:

```csharp
using Elsa.Extensions;
using Elsa.Persistence.Elasticsearch.Extensions;
using Elsa.Persistence.Elasticsearch.Modules.Management;
using Elsa.Persistence.Elasticsearch.Modules.Runtime;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddElsa(elsa =>
{
    elsa.UseElasticsearch(options =>
    {
        // Resolves ConnectionStrings:Elasticsearch from configuration.
        options.Endpoint = "Elasticsearch";
        options.ApiKey = builder.Configuration["Elasticsearch:ApiKey"];
    });

    elsa.UseWorkflowManagement(management =>
    {
        // Workflow definitions are not replaced by this call.
        management.UseElasticsearch();
    });

    elsa.UseWorkflowRuntime(runtime =>
    {
        runtime.UseElasticsearch();
    });

    elsa.UseWorkflowsApi();
});

var app = builder.Build();
app.UseWorkflows();
app.Run();
```

If you only want execution logs in Elasticsearch, omit the management call. If you only want workflow instances there, omit the runtime call. Configure a definition store and the remaining runtime stores separately, using the [persistence overview](/guides/persistence) to choose an approach.

This snippet is intentionally focused on the Elasticsearch registrations; it does not by itself make every Elsa store durable. Treat the companion-store configuration as part of your production persistence design.

## Indexes and naming

The default index name is the dasherized simple document type name:

| Elsa document                | Default index                   |
| ---------------------------- | ------------------------------- |
| `WorkflowInstance`           | `workflow-instance`             |
| `WorkflowExecutionLogRecord` | `workflow-execution-log-record` |

Override names in the shared options when your deployment uses a naming standard or separate indices:

```csharp
using Elsa.Persistence.Elasticsearch.Options;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Runtime.Entities;

elsa.UseElasticsearch(options =>
{
    options.Endpoint = "Elasticsearch";
    options.IndexNameMappings[typeof(WorkflowInstance)] = "elsa-workflow-instances";
    options.IndexNameMappings[typeof(WorkflowExecutionLogRecord)] = "elsa-execution-logs";
});
```

The release configures a flattened mapping for `WorkflowInstance.WorkflowState.Properties`. Treat index mappings as a deployment concern: create and review them before sending production data, and keep index naming/mapping changes compatible with your rollover and retention policy.

## Index lifecycle and deployment checks

The release source contains a `ConfigureClientAsync` hook that can create the workflow-instance index, but the 3.8.0 source tree contains no caller for that hook. Do not assume that registering the package provisions indices or applies your production mappings. Before starting Elsa, verify the following in the target cluster:

1. The endpoint resolves from the deployed configuration.
2. The Elsa identity has permission to read, write, search, and delete the selected indices as required by your operations.
3. The expected indices exist with reviewed mappings, or your cluster's auto-create policy is an intentional part of the deployment.
4. Index templates, rollover, snapshots, and retention are owned by the same operational process that owns the Elasticsearch cluster.
5. A test workflow can be created, queried, updated, and deleted before the provider is used for production workloads.

The extension does not configure Elasticsearch retention, rollover, snapshots, or cluster availability. Use the Elasticsearch operational controls approved by your organization.

## Release-backed limits to test

The Elasticsearch workflow-instance store in 3.8.0 does not implement every `WorkflowInstanceFilter` option. Its source handles the singular identity, version, correlation, status, sub-status, and search-term fields, while collection-based ID filters and parent-instance filters remain TODO. The store's `UpdateUpdatedTimestampAsync` method also throws `NotImplementedException`. Avoid assuming that every Elsa feature that uses a workflow-instance filter or timestamp-only update has the same behavior as an EF Core or MongoDB store.

The execution-log store supports filtering by workflow instance ID, activity ID, and event name. Other log-query requirements should be verified against the API and the release source before you promise them to operators.

## Troubleshooting

### Elsa cannot connect

* Confirm that `Endpoint` is either a valid absolute URI or the exact `ConnectionStrings` key.
* Confirm TLS, DNS, firewall, and cluster health from the Elsa host.
* Confirm that the deployed API key or username/password has not been placed in source control and is accepted by the cluster.

### Documents are rejected or searches return unexpected results

* Inspect the actual index name after applying `IndexNameMappings`.
* Compare the index mapping with the workflow-instance state and execution log documents emitted by your application.
* Check that the request uses a filter supported by the release store; a different persistence provider may support a broader filter set.

### Studio shows incomplete data

The release Studio source has no Elasticsearch provider reference. Studio consumes the Elsa API, so confirm that the server has the intended provider registration, all required companion stores, and the permissions needed by the Studio user. Elasticsearch configuration belongs to the server deployment, not to the Studio browser package.

## Release source

The behavior described here is based on the `release/3.8.0` sources:

* [`ModuleExtensions.cs`](https://github.com/elsa-workflows/elsa-extensions/blob/release/3.8.0/src/modules/persistence/Elsa.Persistence.Elasticsearch/Extensions/ModuleExtensions.cs) and [`ElasticsearchFeature.cs`](https://github.com/elsa-workflows/elsa-extensions/blob/release/3.8.0/src/modules/persistence/Elsa.Persistence.Elasticsearch/Features/ElasticsearchFeature.cs) define endpoint, authentication, and client setup.
* [`ElasticWorkflowInstanceFeature.cs`](https://github.com/elsa-workflows/elsa-extensions/blob/release/3.8.0/src/modules/persistence/Elsa.Persistence.Elasticsearch/Modules/Management/ElasticWorkflowInstanceFeature.cs) and [`WorkflowInstanceStore.cs`](https://github.com/elsa-workflows/elsa-extensions/blob/release/3.8.0/src/modules/persistence/Elsa.Persistence.Elasticsearch/Modules/Management/WorkflowInstanceStore.cs) define the workflow-instance store and its filter behavior.
* [`ElasticExecutionLogRecordFeature.cs`](https://github.com/elsa-workflows/elsa-extensions/blob/release/3.8.0/src/modules/persistence/Elsa.Persistence.Elasticsearch/Modules/Runtime/ElasticExecutionLogRecordFeature.cs) and [`WorkflowExecutionLogStore.cs`](https://github.com/elsa-workflows/elsa-extensions/blob/release/3.8.0/src/modules/persistence/Elsa.Persistence.Elasticsearch/Modules/Runtime/WorkflowExecutionLogStore.cs) define the execution-log store.
* Core's [`WorkflowManagementFeature`](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs) and [`WorkflowRuntimeFeature`](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs) provide the management/runtime composition points.

## Related guidance

* [Persistence overview](/guides/persistence)
* [MongoDB setup](/guides/persistence/mongodb-setup)
* [Dapper setup](/guides/persistence/dapper-setup)
* [Investigate a workflow instance](/operate/workflow-state-and-journal)
* [Monitoring and observability](/operate/monitoring-observability)


# Indexing Notes

Recommended database indexes for Elsa Workflows persistence stores to optimize common query patterns.

This document provides indexing recommendations for Elsa Workflows persistence stores. Proper indexing is essential for production performance.

## Overview

Elsa's persistence layer executes queries against several core tables/collections. Without proper indexes, these queries may result in full table scans, degrading performance under load.

### Key Query Patterns

| Query Pattern               | Tables Involved   | Index Recommendation                    |
| --------------------------- | ----------------- | --------------------------------------- |
| Resume by bookmark hash     | Bookmarks         | `(activity_type_name, hash)`            |
| List instances by status    | WorkflowInstances | `(status)`, `(status, definition_id)`   |
| Correlate instances         | WorkflowInstances | `(correlation_id)`                      |
| Cleanup old instances       | WorkflowInstances | `(updated_at)`, `(finished_at)`         |
| Find bookmarks for instance | Bookmarks         | `(workflow_instance_id)`                |
| List incidents              | Incidents         | `(workflow_instance_id)`, `(timestamp)` |

## PostgreSQL Indexes

### Workflow Instances

```sql
-- Primary lookup by ID (usually covered by PK)
-- CREATE INDEX idx_workflow_instances_id ON workflow_instances(id);

-- Query by correlation ID (very common for HTTP workflows)
CREATE INDEX idx_workflow_instances_correlation_id 
    ON workflow_instances(correlation_id)
    WHERE correlation_id IS NOT NULL;

-- Query by status (list pending, running, faulted, etc.)
CREATE INDEX idx_workflow_instances_status 
    ON workflow_instances(status);

-- Query by definition (list all instances of a workflow)
CREATE INDEX idx_workflow_instances_definition_id 
    ON workflow_instances(definition_id);

-- Composite for filtered status queries
CREATE INDEX idx_workflow_instances_status_definition 
    ON workflow_instances(status, definition_id);

-- Retention queries (cleanup by age)
CREATE INDEX idx_workflow_instances_updated_at 
    ON workflow_instances(updated_at DESC);

CREATE INDEX idx_workflow_instances_finished_at 
    ON workflow_instances(finished_at)
    WHERE finished_at IS NOT NULL;

-- List by sub-status (more granular than status)
CREATE INDEX idx_workflow_instances_sub_status 
    ON workflow_instances(sub_status);
```

### Bookmarks

```sql
-- Primary lookup for resume operations
-- Activity type + hash is the core lookup pattern
CREATE INDEX idx_bookmarks_activity_type_hash 
    ON bookmarks(activity_type_name, hash);

-- Cleanup: find bookmarks for a specific instance
CREATE INDEX idx_bookmarks_workflow_instance_id 
    ON bookmarks(workflow_instance_id);

-- Correlation-based lookups
CREATE INDEX idx_bookmarks_correlation_id 
    ON bookmarks(correlation_id)
    WHERE correlation_id IS NOT NULL;

-- Hash-only lookup (less selective but sometimes used)
CREATE INDEX idx_bookmarks_hash 
    ON bookmarks(hash);
```

### Activity Execution Records

```sql
-- Query by workflow instance (activity history)
CREATE INDEX idx_activity_records_workflow_instance 
    ON activity_execution_records(workflow_instance_id);

-- Query by activity (debugging specific activities)
CREATE INDEX idx_activity_records_activity_id 
    ON activity_execution_records(activity_id);

-- Query by time range (performance analysis)
CREATE INDEX idx_activity_records_started_at 
    ON activity_execution_records(started_at DESC);
```

### Workflow Execution Logs

```sql
-- Query logs for a workflow instance
CREATE INDEX idx_execution_logs_workflow_instance 
    ON workflow_execution_log_records(workflow_instance_id);

-- Query by timestamp (time-series queries)
CREATE INDEX idx_execution_logs_timestamp 
    ON workflow_execution_log_records(timestamp DESC);

-- Composite for instance + time queries
CREATE INDEX idx_execution_logs_instance_timestamp 
    ON workflow_execution_log_records(workflow_instance_id, timestamp DESC);
```

### Incidents

```sql
-- Query incidents for a workflow instance
CREATE INDEX idx_incidents_workflow_instance_id 
    ON incidents(workflow_instance_id);

-- Query by timestamp (monitoring dashboards)
CREATE INDEX idx_incidents_timestamp 
    ON incidents(timestamp DESC);

-- Query by activity (debugging)
CREATE INDEX idx_incidents_activity_id 
    ON incidents(activity_id);
```

### Workflow Inbox Messages

```sql
-- Primary lookup by hash
CREATE INDEX idx_inbox_hash 
    ON workflow_inbox_messages(hash);

-- Correlation lookups
CREATE INDEX idx_inbox_correlation_id 
    ON workflow_inbox_messages(correlation_id)
    WHERE correlation_id IS NOT NULL;

-- Cleanup by age
CREATE INDEX idx_inbox_created_at 
    ON workflow_inbox_messages(created_at);
```

## SQL Server Indexes

SQL Server uses similar index patterns with different syntax:

```sql
-- Workflow Instances
CREATE NONCLUSTERED INDEX idx_workflow_instances_correlation_id 
    ON workflow_instances(correlation_id)
    WHERE correlation_id IS NOT NULL;

CREATE NONCLUSTERED INDEX idx_workflow_instances_status 
    ON workflow_instances(status);

CREATE NONCLUSTERED INDEX idx_workflow_instances_definition_id 
    ON workflow_instances(definition_id);

CREATE NONCLUSTERED INDEX idx_workflow_instances_status_definition 
    ON workflow_instances(status, definition_id);

CREATE NONCLUSTERED INDEX idx_workflow_instances_updated_at 
    ON workflow_instances(updated_at DESC);

-- Bookmarks
CREATE NONCLUSTERED INDEX idx_bookmarks_activity_type_hash 
    ON bookmarks(activity_type_name, hash);

CREATE NONCLUSTERED INDEX idx_bookmarks_workflow_instance_id 
    ON bookmarks(workflow_instance_id);

-- Include columns for covering indexes (reduces key lookups)
CREATE NONCLUSTERED INDEX idx_workflow_instances_status_covering 
    ON workflow_instances(status)
    INCLUDE (id, definition_id, correlation_id, created_at);
```

## MongoDB Indexes

MongoDB requires explicit index creation. Use the MongoDB shell or driver:

```javascript
// Workflow Instances
db.WorkflowInstances.createIndex({ "CorrelationId": 1 });
db.WorkflowInstances.createIndex({ "Status": 1 });
db.WorkflowInstances.createIndex({ "SubStatus": 1 });
db.WorkflowInstances.createIndex({ "DefinitionId": 1 });
db.WorkflowInstances.createIndex({ "Status": 1, "DefinitionId": 1 });
db.WorkflowInstances.createIndex({ "UpdatedAt": -1 });
db.WorkflowInstances.createIndex({ "FinishedAt": 1 }, { 
    partialFilterExpression: { "FinishedAt": { $exists: true } }
});

// Bookmarks
db.Bookmarks.createIndex({ "ActivityTypeName": 1, "Hash": 1 });
db.Bookmarks.createIndex({ "Hash": 1 });
db.Bookmarks.createIndex({ "WorkflowInstanceId": 1 });
db.Bookmarks.createIndex({ "CorrelationId": 1 }, { 
    partialFilterExpression: { "CorrelationId": { $exists: true } }
});

// Activity Execution Records
db.ActivityExecutionRecords.createIndex({ "WorkflowInstanceId": 1 });
db.ActivityExecutionRecords.createIndex({ "ActivityId": 1 });
db.ActivityExecutionRecords.createIndex({ "StartedAt": -1 });

// Workflow Execution Logs
db.WorkflowExecutionLogRecords.createIndex({ "WorkflowInstanceId": 1 });
db.WorkflowExecutionLogRecords.createIndex({ "Timestamp": -1 });
db.WorkflowExecutionLogRecords.createIndex(
    { "WorkflowInstanceId": 1, "Timestamp": -1 }
);

// Incidents
db.Incidents.createIndex({ "WorkflowInstanceId": 1 });
db.Incidents.createIndex({ "Timestamp": -1 });

// Inbox Messages (with TTL)
db.WorkflowInboxMessages.createIndex({ "Hash": 1 });
db.WorkflowInboxMessages.createIndex(
    { "CreatedAt": 1 }, 
    { expireAfterSeconds: 604800 }  // 7 days TTL
);
```

## Index Maintenance

### PostgreSQL

```sql
-- Analyze table statistics (run after bulk operations)
ANALYZE workflow_instances;
ANALYZE bookmarks;

-- Reindex if index bloat is suspected
REINDEX INDEX idx_workflow_instances_status;

-- Check index usage
SELECT schemaname, relname, indexrelname, idx_scan, idx_tup_read
FROM pg_stat_user_indexes
WHERE schemaname = 'elsa'
ORDER BY idx_scan DESC;

-- Find unused indexes
SELECT indexrelname
FROM pg_stat_user_indexes
WHERE idx_scan = 0 AND schemaname = 'elsa';
```

### SQL Server

```sql
-- Update statistics
UPDATE STATISTICS workflow_instances;
UPDATE STATISTICS bookmarks;

-- Rebuild indexes (reduces fragmentation)
ALTER INDEX idx_workflow_instances_status ON workflow_instances REBUILD;

-- Check index fragmentation
SELECT 
    i.name AS IndexName,
    ips.avg_fragmentation_in_percent
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') ips
JOIN sys.indexes i ON ips.object_id = i.object_id AND ips.index_id = i.index_id
WHERE ips.avg_fragmentation_in_percent > 10;
```

### MongoDB

```javascript
// Check index usage
db.WorkflowInstances.aggregate([{ $indexStats: {} }]);

// Compact collection (reclaims space)
db.runCommand({ compact: "WorkflowInstances" });

// Rebuild indexes
db.WorkflowInstances.reIndex();
```

## Performance Monitoring

### Identifying Missing Indexes

**PostgreSQL:**

```sql
-- Find slow queries that might benefit from indexes
SELECT query, calls, mean_exec_time, rows
FROM pg_stat_statements
WHERE query LIKE '%workflow_instances%'
ORDER BY mean_exec_time DESC
LIMIT 10;
```

**SQL Server:**

```sql
-- Missing index recommendations
SELECT 
    d.statement,
    d.equality_columns,
    d.inequality_columns,
    d.included_columns,
    s.avg_user_impact
FROM sys.dm_db_missing_index_details d
JOIN sys.dm_db_missing_index_groups g ON d.index_handle = g.index_handle
JOIN sys.dm_db_missing_index_group_stats s ON g.index_group_handle = s.group_handle
WHERE d.database_id = DB_ID()
ORDER BY s.avg_user_impact DESC;
```

**MongoDB:**

```javascript
// Enable profiler for slow queries
db.setProfilingLevel(1, { slowms: 100 });

// Query profile data
db.system.profile.find({ millis: { $gt: 100 } }).sort({ ts: -1 }).limit(10);
```

## Best Practices

1. **Start with recommended indexes** — Apply the indexes above before production deployment.
2. **Monitor query performance** — Use database-native tools to identify slow queries.
3. **Don't over-index** — Each index adds write overhead and storage. Only add indexes for actual query patterns.
4. **Partial indexes save space** — Use `WHERE` clauses (PostgreSQL) or `partialFilterExpression` (MongoDB) to index only relevant rows.
5. **Covering indexes reduce I/O** — Include frequently accessed columns in the index to avoid table lookups.
6. **Regular maintenance** — Schedule index maintenance during low-traffic periods.
7. **Test with production-like data** — Index performance varies with data distribution. Test with realistic data volumes.

## Vendor Documentation

For detailed index tuning beyond these recommendations:

* [PostgreSQL Indexes](https://www.postgresql.org/docs/current/indexes.html)
* [SQL Server Index Design Guide](https://docs.microsoft.com/en-us/sql/relational-databases/sql-server-index-design-guide)
* [MongoDB Indexes](https://www.mongodb.com/docs/manual/indexes/)

## Related Documentation

* [Persistence Guide](/guides/persistence) — Overview and configuration
* [EF Core Setup](/guides/persistence/efcore-setup) — EF Core configuration
* [MongoDB Setup](/guides/persistence/mongodb-setup) — MongoDB configuration
* [Dapper Setup](/guides/persistence/dapper-setup) — Dapper configuration

***

**Last Updated:** 2025-11-28


# HTTP Workflows

In this guide, we'll take a look at a workflow that can receive HTTP requests, send HTTP requests and write output to the HTTP response object. Our workflow will handle inbound HTTP requests, invoke a backend API using an HTTP call, and write back the response to the client. As a result, we will learn how to use the following HTTP activities:

* HttpEndpoint
* SendHttpRequest
* WriteHttpResponse
* SetVariable

For securing inbound workflow routes exposed by `HttpEndpoint`, see [HTTP Endpoint Security](/guides/security/http-endpoint-security).


# Tutorial

This comprehensive tutorial guides you through creating HTTP-based workflows in Elsa, covering all aspects of HTTP endpoint development from basic concepts to advanced patterns.

## Overview

In this tutorial, you will learn how to:

* Create HTTP endpoints that respond to GET, POST, PUT, and DELETE requests
* Handle query parameters, headers, and request bodies
* Parse and validate incoming data
* Return appropriate HTTP responses with proper status codes
* Implement error handling strategies
* Test and debug HTTP workflows

By the end of this tutorial, you'll have a complete understanding of building production-ready HTTP workflows for RESTful API development.

## Prerequisites

Before you start, ensure you have:

* An [Elsa Server](/application-types/elsa-server) project up and running
* [Elsa Studio](/application-types/elsa-studio) installed and connected to your Elsa Server
* Basic understanding of HTTP methods and REST principles
* A tool for testing HTTP endpoints (Postman, curl, or similar)

{% hint style="info" %}
**New to Elsa?**

If you haven't set up Elsa yet, check out our [Getting Started](/getting-started/hello-world) guide and [Docker Quickstart](/getting-started/containers/docker-compose/docker-quickstart) for the fastest way to get up and running.
{% endhint %}

## Tutorial Scenario

We'll build a simple **Task Management API** with the following endpoints:

* `GET /workflows/tasks` - List all tasks (with query parameters for filtering)
* `GET /workflows/tasks/{id}` - Get a specific task by ID
* `POST /workflows/tasks` - Create a new task
* `PUT /workflows/tasks/{id}` - Update an existing task
* `DELETE /workflows/tasks/{id}` - Delete a task

This scenario will demonstrate real-world patterns you'll use when building HTTP workflows.

## Part 1: Creating a GET Endpoint with Query Parameters

Let's start by creating a workflow that lists tasks with optional filtering via query parameters.

{% stepper %}
{% step %}

#### Create the List Tasks Workflow

1. Open Elsa Studio and navigate to **Workflows**
2. Click **Create Workflow**
3. Name it `ListTasks`
4. Set the workflow as **Published** when ready
   {% endstep %}

{% step %}

#### Add Required Activities

Add the following activities to your workflow:

1. **HTTP Endpoint** - To receive the request
2. **Set Variable** - To extract query parameters
3. **Set Variable** - To create a filtered task list
4. **Write HTTP Response** - To return the results
   {% endstep %}

{% step %}

#### Create Workflow Variables

Create the following variables:

| Name          | Type             | Storage           |
| ------------- | ---------------- | ----------------- |
| QueryData     | ObjectDictionary | Workflow Instance |
| StatusFilter  | string           | Workflow Instance |
| Tasks         | Object           | Workflow Instance |
| {% endstep %} |                  |                   |

{% step %}

#### Configure HTTP Endpoint

Configure the HTTP Endpoint activity:

{% tabs %}
{% tab title="Input" %}

| Property          | Value   | Syntax  |
| ----------------- | ------- | ------- |
| Path              | `tasks` | Default |
| Supported Methods | `GET`   | Default |
| {% endtab %}      |         |         |

{% tab title="Output" %}

| Property          | Value     |
| ----------------- | --------- |
| Query String Data | QueryData |
| {% endtab %}      |           |

{% tab title="Common" %}

| Property         | Value   |
| ---------------- | ------- |
| Trigger Workflow | Checked |
| {% endtab %}     |         |
| {% endtabs %}    |         |
| {% endstep %}    |         |

{% step %}

#### Extract Query Parameters

Configure the first Set Variable activity to extract the status filter:

{% tabs %}
{% tab title="Input" %}

| Property      | Value                                       | Syntax  |
| ------------- | ------------------------------------------- | ------- |
| Variable      | `StatusFilter`                              | Default |
| Value         | `{{ Variables.QueryData.status ?? "all" }}` | Liquid  |
| {% endtab %}  |                                             |         |
| {% endtabs %} |                                             |         |

This extracts the `status` query parameter (e.g., `?status=active`) or defaults to "all".
{% endstep %}

{% step %}

#### Create Mock Task Data

Configure the second Set Variable activity to create sample task data:

{% tabs %}
{% tab title="Input" %}

| Property      | Value          | Syntax  |
| ------------- | -------------- | ------- |
| Variable      | `Tasks`        | Default |
| Value         | See code below | C#      |
| {% endtab %}  |                |         |
| {% endtabs %} |                |         |

C# Expression:

```csharp
var allTasks = new[]
{
    new { Id = 1, Title = "Complete documentation", Status = "active", Priority = "high" },
    new { Id = 2, Title = "Review pull requests", Status = "active", Priority = "medium" },
    new { Id = 3, Title = "Update dependencies", Status = "completed", Priority = "low" },
    new { Id = 4, Title = "Fix critical bug", Status = "active", Priority = "high" },
    new { Id = 5, Title = "Deploy to production", Status = "pending", Priority = "high" }
};

var filter = Variables.StatusFilter.ToLower();
return filter == "all" 
    ? allTasks 
    : allTasks.Where(t => t.Status.ToLower() == filter).ToArray();
```

{% endstep %}

{% step %}

#### Return Response

Configure the Write HTTP Response activity:

{% tabs %}
{% tab title="Input" %}

| Property      | Value              | Syntax     |
| ------------- | ------------------ | ---------- |
| Status Code   | `OK`               | Default    |
| Content       | `Variables.Tasks`  | JavaScript |
| Content Type  | `application/json` | Default    |
| {% endtab %}  |                    |            |
| {% endtabs %} |                    |            |
| {% endstep %} |                    |            |

{% step %}

#### Test the Workflow

1. **Publish** the workflow
2. Test with different query parameters:
   * `GET https://localhost:5001/workflows/tasks` - Returns all tasks
   * `GET https://localhost:5001/workflows/tasks?status=active` - Returns only active tasks
   * `GET https://localhost:5001/workflows/tasks?status=completed` - Returns completed tasks

{% hint style="success" %}
**Expected Response**

When you make a request to `https://localhost:5001/workflows/tasks?status=active`, you should receive a JSON response containing only the active tasks:

```json
[
  {
    "Id": 1,
    "Title": "Complete documentation",
    "Status": "active",
    "Priority": "high"
  },
  {
    "Id": 2,
    "Title": "Review pull requests",
    "Status": "active",
    "Priority": "medium"
  },
  {
    "Id": 4,
    "Title": "Fix critical bug",
    "Status": "active",
    "Priority": "high"
  }
]
```

{% endhint %}
{% endstep %}
{% endstepper %}

## Part 2: Creating a GET Endpoint with Route Parameters

Now let's create a workflow that retrieves a specific task by ID using route parameters.

{% stepper %}
{% step %}

#### Create the Get Task Workflow

1. Create a new workflow named `GetTask`
2. This workflow will handle requests like `GET /workflows/tasks/1`
   {% endstep %}

{% step %}

#### Create Variables

| Name          | Type             | Storage           |
| ------------- | ---------------- | ----------------- |
| RouteData     | ObjectDictionary | Workflow Instance |
| TaskId        | string           | Workflow Instance |
| Task          | Object           | Workflow Instance |
| {% endstep %} |                  |                   |

{% step %}

#### Configure HTTP Endpoint

{% tabs %}
{% tab title="Input" %}

| Property          | Value        | Syntax  |
| ----------------- | ------------ | ------- |
| Path              | `tasks/{id}` | Default |
| Supported Methods | `GET`        | Default |
| {% endtab %}      |              |         |

{% tab title="Output" %}

| Property     | Value     |
| ------------ | --------- |
| Route Data   | RouteData |
| {% endtab %} |           |

{% tab title="Common" %}

| Property         | Value   |
| ---------------- | ------- |
| Trigger Workflow | Checked |
| {% endtab %}     |         |
| {% endtabs %}    |         |
| {% endstep %}    |         |

{% step %}

#### Extract Task ID

Add a Set Variable activity:

{% tabs %}
{% tab title="Input" %}

| Property      | Value                          | Syntax  |
| ------------- | ------------------------------ | ------- |
| Variable      | `TaskId`                       | Default |
| Value         | `{{ Variables.RouteData.id }}` | Liquid  |
| {% endtab %}  |                                |         |
| {% endtabs %} |                                |         |
| {% endstep %} |                                |         |

{% step %}

#### Find Task

Add another Set Variable activity with branching logic:

{% tabs %}
{% tab title="Input" %}

| Property      | Value          | Syntax  |
| ------------- | -------------- | ------- |
| Variable      | `Task`         | Default |
| Value         | See code below | C#      |
| {% endtab %}  |                |         |
| {% endtabs %} |                |         |

C# Expression:

```csharp
var tasks = new[]
{
    new { Id = 1, Title = "Complete documentation", Status = "active", Priority = "high", Description = "Write comprehensive HTTP workflows tutorial" },
    new { Id = 2, Title = "Review pull requests", Status = "active", Priority = "medium", Description = "Review and merge pending PRs" },
    new { Id = 3, Title = "Update dependencies", Status = "completed", Priority = "low", Description = "Update NuGet packages" }
};

var taskId = int.Parse(Variables.TaskId);
return tasks.FirstOrDefault(t => t.Id == taskId);
```

{% endstep %}

{% step %}

#### Add Conditional Response

Add a **Decision** activity to check if the task was found:

{% tabs %}
{% tab title="Input" %}

| Property      | Value                    | Syntax |
| ------------- | ------------------------ | ------ |
| Condition     | `Variables.Task != null` | C#     |
| {% endtab %}  |                          |        |
| {% endtabs %} |                          |        |

Connect two Write HTTP Response activities to the Decision outcomes:

**For "True" outcome (Task Found):**

{% tabs %}
{% tab title="Input" %}

| Property      | Value              | Syntax     |
| ------------- | ------------------ | ---------- |
| Status Code   | `OK`               | Default    |
| Content       | `Variables.Task`   | JavaScript |
| Content Type  | `application/json` | Default    |
| {% endtab %}  |                    |            |
| {% endtabs %} |                    |            |

**For "False" outcome (Task Not Found):**

{% tabs %}
{% tab title="Input" %}

| Property      | Value                                                           | Syntax  |
| ------------- | --------------------------------------------------------------- | ------- |
| Status Code   | `NotFound`                                                      | Default |
| Content       | `{"error": "Task not found", "taskId": "{{Variables.TaskId}}"}` | Liquid  |
| Content Type  | `application/json`                                              | Default |
| {% endtab %}  |                                                                 |         |
| {% endtabs %} |                                                                 |         |
| {% endstep %} |                                                                 |         |

{% step %}

#### Test the Workflow

Test with different task IDs:

* `GET https://localhost:5001/workflows/tasks/1` - Returns task details (200 OK)
* `GET https://localhost:5001/workflows/tasks/999` - Returns error message (404 Not Found)
  {% endstep %}
  {% endstepper %}

## Part 3: Creating a POST Endpoint for Creating Resources

Let's create a workflow that handles POST requests to create new tasks.

{% stepper %}
{% step %}

#### Create the Create Task Workflow

Create a new workflow named `CreateTask`
{% endstep %}

{% step %}

#### Create Variables

| Name             | Type   | Storage           |
| ---------------- | ------ | ----------------- |
| RequestBody      | Object | Workflow Instance |
| NewTask          | Object | Workflow Instance |
| ValidationErrors | Object | Workflow Instance |
| {% endstep %}    |        |                   |

{% step %}

#### Configure HTTP Endpoint

{% tabs %}
{% tab title="Input" %}

| Property          | Value   | Syntax  |
| ----------------- | ------- | ------- |
| Path              | `tasks` | Default |
| Supported Methods | `POST`  | Default |
| {% endtab %}      |         |         |

{% tab title="Output" %}

| Property       | Value       |
| -------------- | ----------- |
| Parsed Content | RequestBody |
| {% endtab %}   |             |

{% tab title="Common" %}

| Property         | Value   |
| ---------------- | ------- |
| Trigger Workflow | Checked |
| {% endtab %}     |         |
| {% endtabs %}    |         |

The HTTP Endpoint automatically parses JSON request bodies into the `Parsed Content` output.
{% endstep %}

{% step %}

#### Validate Request Body

Add a Set Variable activity to validate the input:

{% tabs %}
{% tab title="Input" %}

| Property      | Value              | Syntax  |
| ------------- | ------------------ | ------- |
| Variable      | `ValidationErrors` | Default |
| Value         | See code below     | C#      |
| {% endtab %}  |                    |         |
| {% endtabs %} |                    |         |

C# Expression:

```csharp
var errors = new List<string>();
var body = (dynamic)Variables.RequestBody;

if (body == null)
{
    errors.Add("Request body is required");
    return new { Errors = errors };
}

if (string.IsNullOrWhiteSpace(body.Title?.ToString()))
    errors.Add("Title is required");

if (string.IsNullOrWhiteSpace(body.Status?.ToString()))
    errors.Add("Status is required");
else
{
    var validStatuses = new[] { "active", "pending", "completed" };
    if (!validStatuses.Contains(body.Status.ToString().ToLower()))
        errors.Add("Status must be one of: active, pending, completed");
}

return errors.Any() ? new { Errors = errors } : null;
```

{% endstep %}

{% step %}

#### Add Validation Decision

Add a **Decision** activity:

{% tabs %}
{% tab title="Input" %}

| Property      | Value                                | Syntax |
| ------------- | ------------------------------------ | ------ |
| Condition     | `Variables.ValidationErrors == null` | C#     |
| {% endtab %}  |                                      |        |
| {% endtabs %} |                                      |        |
| {% endstep %} |                                      |        |

{% step %}

#### Create Task (Valid Input)

For the "True" outcome, add a Set Variable activity:

{% tabs %}
{% tab title="Input" %}

| Property      | Value          | Syntax  |
| ------------- | -------------- | ------- |
| Variable      | `NewTask`      | Default |
| Value         | See code below | C#      |
| {% endtab %}  |                |         |
| {% endtabs %} |                |         |

C# Expression:

```csharp
var body = (dynamic)Variables.RequestBody;

// For demonstration: simple sequential ID
// In production, use:
// - Database auto-increment IDs for sequential IDs
// - Guid.NewGuid() for globally unique identifiers  
// - Snowflake IDs for distributed systems
// - ID generation service for complex requirements
var demoId = DateTime.UtcNow.Ticks % 100000; // Demo: timestamp-based ID

return new
{
    Id = (int)demoId,
    Title = body.Title.ToString(),
    Status = body.Status.ToString().ToLower(),
    Priority = body.Priority?.ToString()?.ToLower() ?? "medium",
    Description = body.Description?.ToString() ?? "",
    CreatedAt = DateTime.UtcNow,
    UpdatedAt = DateTime.UtcNow
};
```

Then add a Write HTTP Response activity:

{% tabs %}
{% tab title="Input" %}

| Property     | Value               | Syntax     |
| ------------ | ------------------- | ---------- |
| Status Code  | `Created`           | Default    |
| Content      | `Variables.NewTask` | JavaScript |
| Content Type | `application/json`  | Default    |
| {% endtab %} |                     |            |

{% tab title="Headers" %}
Add a custom header:

* **Name**: `Location`
* **Value**: `/workflows/tasks/{{Variables.NewTask.Id}}` (Liquid)

{% hint style="info" %}
**Location Header**

In production, build the full URL dynamically using the request's base URL. The relative path shown here works for most scenarios and avoids hardcoding domain names.
{% endhint %}
{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}

#### Return Validation Errors (Invalid Input)

For the "False" outcome, add a Write HTTP Response activity:

{% tabs %}
{% tab title="Input" %}

| Property      | Value                        | Syntax     |
| ------------- | ---------------------------- | ---------- |
| Status Code   | `BadRequest`                 | Default    |
| Content       | `Variables.ValidationErrors` | JavaScript |
| Content Type  | `application/json`           | Default    |
| {% endtab %}  |                              |            |
| {% endtabs %} |                              |            |
| {% endstep %} |                              |            |

{% step %}

#### Test the Workflow

Test with valid data:

```bash
curl -X POST https://localhost:5001/workflows/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "title": "New Task",
    "status": "active",
    "priority": "high",
    "description": "Task description"
  }'
```

Test with invalid data:

```bash
curl -X POST https://localhost:5001/workflows/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "status": "invalid"
  }'
```

{% endstep %}
{% endstepper %}

## Part 4: Creating a PUT Endpoint for Updates

Let's create a workflow that handles PUT requests to update existing tasks.

{% stepper %}
{% step %}

#### Create the Update Task Workflow

Create a new workflow named `UpdateTask`
{% endstep %}

{% step %}

#### Create Variables

| Name          | Type             | Storage           |
| ------------- | ---------------- | ----------------- |
| RouteData     | ObjectDictionary | Workflow Instance |
| RequestBody   | Object           | Workflow Instance |
| TaskId        | string           | Workflow Instance |
| ExistingTask  | Object           | Workflow Instance |
| UpdatedTask   | Object           | Workflow Instance |
| {% endstep %} |                  |                   |

{% step %}

#### Configure HTTP Endpoint

{% tabs %}
{% tab title="Input" %}

| Property          | Value        | Syntax  |
| ----------------- | ------------ | ------- |
| Path              | `tasks/{id}` | Default |
| Supported Methods | `PUT`        | Default |
| {% endtab %}      |              |         |

{% tab title="Output" %}

| Property       | Value       |
| -------------- | ----------- |
| Route Data     | RouteData   |
| Parsed Content | RequestBody |
| {% endtab %}   |             |

{% tab title="Common" %}

| Property         | Value   |
| ---------------- | ------- |
| Trigger Workflow | Checked |
| {% endtab %}     |         |
| {% endtabs %}    |         |
| {% endstep %}    |         |

{% step %}

#### Extract Task ID

Add a Set Variable activity:

{% tabs %}
{% tab title="Input" %}

| Property      | Value                          | Syntax  |
| ------------- | ------------------------------ | ------- |
| Variable      | `TaskId`                       | Default |
| Value         | `{{ Variables.RouteData.id }}` | Liquid  |
| {% endtab %}  |                                |         |
| {% endtabs %} |                                |         |
| {% endstep %} |                                |         |

{% step %}

#### Find Existing Task

Add a Set Variable activity:

{% tabs %}
{% tab title="Input" %}

| Property      | Value          | Syntax  |
| ------------- | -------------- | ------- |
| Variable      | `ExistingTask` | Default |
| Value         | See code below | C#      |
| {% endtab %}  |                |         |
| {% endtabs %} |                |         |

C# Expression:

```csharp
var tasks = new[]
{
    new { Id = 1, Title = "Complete documentation", Status = "active", Priority = "high", CreatedAt = DateTime.UtcNow.AddDays(-5) },
    new { Id = 2, Title = "Review pull requests", Status = "active", Priority = "medium", CreatedAt = DateTime.UtcNow.AddDays(-3) }
};

var taskId = int.Parse(Variables.TaskId);
return tasks.FirstOrDefault(t => t.Id == taskId);
```

{% endstep %}

{% step %}

#### Add Decision for Task Existence

Add a **Decision** activity:

{% tabs %}
{% tab title="Input" %}

| Property      | Value                            | Syntax |
| ------------- | -------------------------------- | ------ |
| Condition     | `Variables.ExistingTask != null` | C#     |
| {% endtab %}  |                                  |        |
| {% endtabs %} |                                  |        |
| {% endstep %} |                                  |        |

{% step %}

#### Update Task (If Found)

For the "True" outcome, add a Set Variable activity:

{% tabs %}
{% tab title="Input" %}

| Property      | Value          | Syntax  |
| ------------- | -------------- | ------- |
| Variable      | `UpdatedTask`  | Default |
| Value         | See code below | C#      |
| {% endtab %}  |                |         |
| {% endtabs %} |                |         |

C# Expression:

```csharp
var existing = (dynamic)Variables.ExistingTask;
var updates = (dynamic)Variables.RequestBody;

return new
{
    Id = existing.Id,
    Title = updates.Title?.ToString() ?? existing.Title.ToString(),
    Status = updates.Status?.ToString()?.ToLower() ?? existing.Status.ToString(),
    Priority = updates.Priority?.ToString()?.ToLower() ?? existing.Priority.ToString(),
    Description = updates.Description?.ToString() ?? "",
    CreatedAt = existing.CreatedAt,
    UpdatedAt = DateTime.UtcNow
};
```

Then add a Write HTTP Response activity:

{% tabs %}
{% tab title="Input" %}

| Property      | Value                   | Syntax     |
| ------------- | ----------------------- | ---------- |
| Status Code   | `OK`                    | Default    |
| Content       | `Variables.UpdatedTask` | JavaScript |
| Content Type  | `application/json`      | Default    |
| {% endtab %}  |                         |            |
| {% endtabs %} |                         |            |
| {% endstep %} |                         |            |

{% step %}

#### Return Not Found (If Task Doesn't Exist)

For the "False" outcome, add a Write HTTP Response activity:

{% tabs %}
{% tab title="Input" %}

| Property      | Value                                                           | Syntax  |
| ------------- | --------------------------------------------------------------- | ------- |
| Status Code   | `NotFound`                                                      | Default |
| Content       | `{"error": "Task not found", "taskId": "{{Variables.TaskId}}"}` | Liquid  |
| Content Type  | `application/json`                                              | Default |
| {% endtab %}  |                                                                 |         |
| {% endtabs %} |                                                                 |         |
| {% endstep %} |                                                                 |         |

{% step %}

#### Test the Workflow

Test updating an existing task:

```bash
curl -X PUT https://localhost:5001/workflows/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Updated Task Title",
    "status": "completed"
  }'
```

Test updating a non-existent task:

```bash
curl -X PUT https://localhost:5001/workflows/tasks/999 \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Updated Task"
  }'
```

{% endstep %}
{% endstepper %}

## Part 5: Creating a DELETE Endpoint

Let's complete our CRUD operations with a DELETE endpoint.

{% stepper %}
{% step %}

#### Create the Delete Task Workflow

Create a new workflow named `DeleteTask`
{% endstep %}

{% step %}

#### Create Variables

| Name          | Type             | Storage           |
| ------------- | ---------------- | ----------------- |
| RouteData     | ObjectDictionary | Workflow Instance |
| TaskId        | string           | Workflow Instance |
| TaskExists    | bool             | Workflow Instance |
| {% endstep %} |                  |                   |

{% step %}

#### Configure HTTP Endpoint

{% tabs %}
{% tab title="Input" %}

| Property          | Value        | Syntax  |
| ----------------- | ------------ | ------- |
| Path              | `tasks/{id}` | Default |
| Supported Methods | `DELETE`     | Default |
| {% endtab %}      |              |         |

{% tab title="Output" %}

| Property     | Value     |
| ------------ | --------- |
| Route Data   | RouteData |
| {% endtab %} |           |

{% tab title="Common" %}

| Property         | Value   |
| ---------------- | ------- |
| Trigger Workflow | Checked |
| {% endtab %}     |         |
| {% endtabs %}    |         |
| {% endstep %}    |         |

{% step %}

#### Extract and Validate Task ID

Add a Set Variable activity to extract the ID:

{% tabs %}
{% tab title="Input" %}

| Property      | Value                          | Syntax  |
| ------------- | ------------------------------ | ------- |
| Variable      | `TaskId`                       | Default |
| Value         | `{{ Variables.RouteData.id }}` | Liquid  |
| {% endtab %}  |                                |         |
| {% endtabs %} |                                |         |

Then add another Set Variable activity to check if task exists:

{% tabs %}
{% tab title="Input" %}

| Property      | Value          | Syntax  |
| ------------- | -------------- | ------- |
| Variable      | `TaskExists`   | Default |
| Value         | See code below | C#      |
| {% endtab %}  |                |         |
| {% endtabs %} |                |         |

C# Expression:

```csharp
var existingTaskIds = new[] { 1, 2, 3, 4, 5 };
var taskId = int.Parse(Variables.TaskId);
return existingTaskIds.Contains(taskId);
```

{% endstep %}

{% step %}

#### Add Decision

Add a **Decision** activity:

{% tabs %}
{% tab title="Input" %}

| Property      | Value                  | Syntax |
| ------------- | ---------------------- | ------ |
| Condition     | `Variables.TaskExists` | C#     |
| {% endtab %}  |                        |        |
| {% endtabs %} |                        |        |
| {% endstep %} |                        |        |

{% step %}

#### Return Success (If Deleted)

For the "True" outcome, add a Write HTTP Response activity:

{% tabs %}
{% tab title="Input" %}

| Property      | Value       | Syntax  |
| ------------- | ----------- | ------- |
| Status Code   | `NoContent` | Default |
| {% endtab %}  |             |         |
| {% endtabs %} |             |         |

{% hint style="info" %}
**HTTP 204 No Content**

The 204 status code indicates successful deletion without returning any content in the response body. This is the standard practice for DELETE operations.
{% endhint %}
{% endstep %}

{% step %}

#### Return Not Found (If Task Doesn't Exist)

For the "False" outcome, add a Write HTTP Response activity:

{% tabs %}
{% tab title="Input" %}

| Property      | Value                                                           | Syntax  |
| ------------- | --------------------------------------------------------------- | ------- |
| Status Code   | `NotFound`                                                      | Default |
| Content       | `{"error": "Task not found", "taskId": "{{Variables.TaskId}}"}` | Liquid  |
| Content Type  | `application/json`                                              | Default |
| {% endtab %}  |                                                                 |         |
| {% endtabs %} |                                                                 |         |
| {% endstep %} |                                                                 |         |

{% step %}

#### Test the Workflow

Test deleting an existing task:

```bash
curl -X DELETE https://localhost:5001/workflows/tasks/1
```

Test deleting a non-existent task:

```bash
curl -X DELETE https://localhost:5001/workflows/tasks/999
```

{% endstep %}
{% endstepper %}

## Part 6: Working with Headers

Learn how to read and set HTTP headers in your workflows.

### Reading Request Headers

To access request headers, use the HTTP Endpoint activity's **Headers** output:

{% stepper %}
{% step %}

#### Create Variables

| Name          | Type             | Storage           |
| ------------- | ---------------- | ----------------- |
| Headers       | ObjectDictionary | Workflow Instance |
| AuthToken     | string           | Workflow Instance |
| UserAgent     | string           | Workflow Instance |
| {% endstep %} |                  |                   |

{% step %}

#### Configure HTTP Endpoint

{% tabs %}
{% tab title="Output" %}

| Property      | Value   |
| ------------- | ------- |
| Headers       | Headers |
| {% endtab %}  |         |
| {% endtabs %} |         |
| {% endstep %} |         |

{% step %}

#### Extract Header Values

Add Set Variable activities to extract specific headers:

**For Authorization Header:**

{% tabs %}
{% tab title="Input" %}

| Property      | Value                                                          | Syntax  |
| ------------- | -------------------------------------------------------------- | ------- |
| Variable      | `AuthToken`                                                    | Default |
| Value         | `{{ Variables.Headers.Authorization ?? "No token provided" }}` | Liquid  |
| {% endtab %}  |                                                                |         |
| {% endtabs %} |                                                                |         |

**For User-Agent Header:**

{% tabs %}
{% tab title="Input" %}

| Property         | Value                                                | Syntax  |
| ---------------- | ---------------------------------------------------- | ------- |
| Variable         | `UserAgent`                                          | Default |
| Value            | `{{ Variables.Headers["User-Agent"] ?? "Unknown" }}` | Liquid  |
| {% endtab %}     |                                                      |         |
| {% endtabs %}    |                                                      |         |
| {% endstep %}    |                                                      |         |
| {% endstepper %} |                                                      |         |

### Setting Response Headers

To set custom response headers, configure the Write HTTP Response activity:

{% tabs %}
{% tab title="Headers" %}
Add custom headers:

| Name            | Value                                 | Syntax                        |
| --------------- | ------------------------------------- | ----------------------------- |
| X-Request-Id    | `{{guid()}}`                          | Liquid                        |
| X-Response-Time | \`{{now                               | date: "%Y-%m-%d %H:%M:%S"}}\` |
| Cache-Control   | `no-cache, no-store, must-revalidate` | Default                       |
| X-Api-Version   | `v3`                                  | Default                       |
| {% endtab %}    |                                       |                               |
| {% endtabs %}   |                                       |                               |

## Part 7: Error Handling Strategies

Implement robust error handling to make your workflows production-ready.

### Pattern 1: Emit a Workflow Fault

Use the **Fault** activity when the workflow should stop with a structured fault that can be processed by Elsa's incident handling pipeline.

{% stepper %}
{% step %}

#### Use Fault Activity

Add a **Fault** activity on the branch where the workflow has detected an unrecoverable business or integration error:

1. Add a **Fault** activity
2. Configure its **Code**, **Category**, **Type**, and optional **Message** inputs
3. Configure incident handling or resilience behavior for activities that may fail before this point
   {% endstep %}

{% step %}

#### Build the Fault Message

Create a Set Variable activity before the Fault activity if you need to assemble a reusable error message:

{% tabs %}
{% tab title="Input" %}

| Property      | Value           | Syntax  |
| ------------- | --------------- | ------- |
| Variable      | `ErrorResponse` | Default |
| Value         | See code below  | C#      |
| {% endtab %}  |                 |         |
| {% endtabs %} |                 |         |

C# Expression:

```csharp
return new
{
    Error = "An error occurred while processing your request",
    Timestamp = DateTime.UtcNow,
    RequestId = Guid.NewGuid().ToString()
};
```

{% endstep %}

{% step %}

#### Return Error Response

Add a Write HTTP Response activity:

{% tabs %}
{% tab title="Input" %}

| Property         | Value                     | Syntax     |
| ---------------- | ------------------------- | ---------- |
| Status Code      | `InternalServerError`     | Default    |
| Content          | `Variables.ErrorResponse` | JavaScript |
| Content Type     | `application/json`        | Default    |
| {% endtab %}     |                           |            |
| {% endtabs %}    |                           |            |
| {% endstep %}    |                           |            |
| {% endstepper %} |                           |            |

### Pattern 2: Validation and Early Returns

Validate input early and return appropriate error responses:

```csharp
// Validation example
var body = (dynamic)Variables.RequestBody;
var errors = new List<object>();

// Check required fields
if (string.IsNullOrWhiteSpace(body?.Title?.ToString()))
    errors.Add(new { Field = "title", Message = "Title is required" });

// Check field formats
if (body?.Email != null)
{
    var email = body.Email.ToString();
    if (!email.Contains("@"))
        errors.Add(new { Field = "email", Message = "Invalid email format" });
}

// Check field lengths
if (body?.Title?.ToString()?.Length > 100)
    errors.Add(new { Field = "title", Message = "Title must be 100 characters or less" });

return errors.Any() ? new { ValidationErrors = errors } : null;
```

### Pattern 3: Custom Error Status Codes

Use appropriate HTTP status codes for different error scenarios:

| Status Code               | Use Case                          | Example                                  |
| ------------------------- | --------------------------------- | ---------------------------------------- |
| 400 Bad Request           | Invalid input data                | Missing required fields, invalid format  |
| 401 Unauthorized          | Missing or invalid authentication | No auth token provided                   |
| 403 Forbidden             | Insufficient permissions          | User not allowed to perform action       |
| 404 Not Found             | Resource doesn't exist            | Task ID not found                        |
| 409 Conflict              | Resource state conflict           | Task already exists                      |
| 422 Unprocessable Entity  | Semantic validation errors        | Valid format but business rule violation |
| 429 Too Many Requests     | Rate limit exceeded               | Too many API calls                       |
| 500 Internal Server Error | Unexpected server errors          | Database connection failure              |
| 503 Service Unavailable   | Temporary service issues          | Downstream service unavailable           |

## Part 8: Advanced Request/Response Patterns

### Content Negotiation

Handle different content types based on request headers:

{% hint style="info" %}
**Conceptual Example**

The following demonstrates the concept of content negotiation. In a real implementation, you would need to:

* Implement XML/CSV serialization methods based on your needs
* Use libraries like System.Xml.Serialization or CsvHelper
* Configure appropriate Content-Type headers in Write HTTP Response activity
  {% endhint %}

```csharp
var headers = Variables.Headers;
var acceptHeader = headers.ContainsKey("Accept") ? headers["Accept"].ToString() : "application/json";

if (acceptHeader.Contains("application/xml"))
{
    // Pseudo-code: XML serialization pattern
    var task = (dynamic)Variables.Task;
    var xml = $@"<?xml version=""1.0"" encoding=""UTF-8""?>
<task>
    <id>{task.Id}</id>
    <title>{task.Title}</title>
    <status>{task.Status}</status>
</task>";
    // In production, use System.Xml.Serialization.XmlSerializer
    // or System.Xml.Linq.XDocument for proper serialization
    return new { ContentType = "application/xml", Body = xml };
}
else if (acceptHeader.Contains("text/csv"))
{
    // Pseudo-code: CSV serialization pattern
    var tasks = (System.Collections.IEnumerable)Variables.Tasks;
    var csv = "Id,Title,Status\n";
    foreach (dynamic task in tasks)
    {
        csv += $"{task.Id},{task.Title},{task.Status}\n";
    }
    // In production, use CsvHelper library for robust CSV generation
    return new { ContentType = "text/csv", Body = csv };
}
else
{
    // Default to JSON (handled automatically by Elsa)
    return new { ContentType = "application/json", Body = Variables.Task };
}
```

### CORS Headers

Enable Cross-Origin Resource Sharing (CORS) for browser-based clients:

{% tabs %}
{% tab title="Response Headers" %}

| Name                         | Value                             |
| ---------------------------- | --------------------------------- |
| Access-Control-Allow-Origin  | `https://yourdomain.com`          |
| Access-Control-Allow-Methods | `GET, POST, PUT, DELETE, OPTIONS` |
| Access-Control-Allow-Headers | `Content-Type, Authorization`     |
| Access-Control-Max-Age       | `3600`                            |
| {% endtab %}                 |                                   |
| {% endtabs %}                |                                   |

{% hint style="warning" %}
**CORS Security**

* Never use `*` for `Access-Control-Allow-Origin` in production, especially with credentials
* Always specify the exact allowed origin domain(s)
* For multiple domains, implement logic to validate and return the requesting origin
* Consider security implications before enabling CORS
  {% endhint %}

### Pagination

Implement pagination for list endpoints:

```csharp
var queryData = Variables.QueryData;
var page = queryData.ContainsKey("page") ? int.Parse(queryData["page"].ToString()) : 1;
var pageSize = queryData.ContainsKey("pageSize") ? int.Parse(queryData["pageSize"].ToString()) : 10;

// Limit page size
pageSize = Math.Min(pageSize, 100);

// Replace this with your actual data source:
// - Database query with Skip/Take
// - API call to backend service
// - Workflow variable containing your data collection
var allTasks = new[]
{
    new { Id = 1, Title = "Task 1", Status = "active" },
    new { Id = 2, Title = "Task 2", Status = "pending" },
    new { Id = 3, Title = "Task 3", Status = "completed" },
    // ... more tasks
}.AsQueryable();

var totalCount = allTasks.Count();
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);

var pagedTasks = allTasks
    .Skip((page - 1) * pageSize)
    .Take(pageSize)
    .ToList();

return new
{
    Data = pagedTasks,
    Pagination = new
    {
        Page = page,
        PageSize = pageSize,
        TotalCount = totalCount,
        TotalPages = totalPages,
        HasNextPage = page < totalPages,
        HasPreviousPage = page > 1
    }
};
```

### Rate Limiting

Track and limit request rates per client:

{% hint style="warning" %}
**Rate Limiting Considerations**

Rate limiting in production requires careful implementation:

**Infrastructure:**

* Use API Gateway features (Azure API Management, AWS API Gateway, Kong)
* Implement with distributed cache (Redis, Memcached)
* Use ASP.NET Core Rate Limiting middleware
* Leverage CDN/WAF services (Cloudflare, etc.)

**Client Identification:**

* ⚠️ **Avoid IP-based limiting alone**: IPs can be shared (NAT, proxies, mobile networks)
* ✅ **Prefer authenticated identifiers**: User IDs, API keys, OAuth tokens
* ✅ **Validate proxy headers**: Only trust X-Forwarded-For from known proxies
* ✅ **Combine methods**: Use both authentication and IP for better accuracy

The example below demonstrates the concept but requires proper implementation.
{% endhint %}

```csharp
// Conceptual demonstration of rate limiting logic
// In production, implement caching with Redis or similar:
// - IDistributedCache for ASP.NET Core
// - StackExchange.Redis for direct Redis access
// - Built-in ASP.NET Core rate limiting middleware

var clientId = "demo-client"; // Replace with: authenticated user ID, API key, or validated IP
var requestKey = $"rate_limit:{clientId}";
var maxRequests = 100; // per hour
var windowSeconds = 3600;

// Pseudo-code: Implement these with your caching solution
// Example with IDistributedCache:
// var cacheValue = await _cache.GetStringAsync(requestKey);
// var requestCount = string.IsNullOrEmpty(cacheValue) ? 0 : int.Parse(cacheValue);
var requestCount = 0; // Placeholder: retrieve from your cache

if (requestCount >= maxRequests)
{
    return new
    {
        StatusCode = 429,
        Error = "Rate limit exceeded",
        RetryAfter = windowSeconds,
        Limit = maxRequests,
        Remaining = 0
    };
}

// Pseudo-code: Implement cache increment
// Example: await _cache.SetStringAsync(requestKey, (requestCount + 1).ToString(), 
//              new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(windowSeconds) });

return new
{
    StatusCode = 200,
    Limit = maxRequests,
    Remaining = maxRequests - requestCount - 1
};
```

## Part 9: Testing Your HTTP Workflows

### Using Postman

1. **Create a Collection**: Organize all your workflow endpoints
2. **Set Environment Variables**: Configure base URL, auth tokens
3. **Write Tests**: Add test scripts to validate responses

Example Postman test script:

```javascript
pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});

pm.test("Response has correct structure", function () {
    var jsonData = pm.response.json();
    pm.expect(jsonData).to.have.property('id');
    pm.expect(jsonData).to.have.property('title');
    pm.expect(jsonData.status).to.be.oneOf(['active', 'pending', 'completed']);
});
```

### Using cURL

Test your endpoints from the command line:

```bash
# List all tasks
curl -X GET https://localhost:5001/workflows/tasks

# Get specific task
curl -X GET https://localhost:5001/workflows/tasks/1

# Create task
curl -X POST https://localhost:5001/workflows/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"New Task","status":"active"}'

# Update task
curl -X PUT https://localhost:5001/workflows/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"Updated Task","status":"completed"}'

# Delete task
curl -X DELETE https://localhost:5001/workflows/tasks/1

# With custom headers
curl -X GET https://localhost:5001/workflows/tasks \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Request-Id: 12345"

# Verbose output (see headers)
curl -v -X GET https://localhost:5001/workflows/tasks/1
```

### Using HTTP Files (REST Client)

Create a `.http` file for testing (replace `{{baseUrl}}` with your server URL):

```http
@baseUrl = https://localhost:5001

### List all tasks
GET {{baseUrl}}/workflows/tasks

### Get specific task
GET {{baseUrl}}/workflows/tasks/1

### Create task
POST {{baseUrl}}/workflows/tasks
Content-Type: application/json

{
  "title": "New Task",
  "status": "active",
  "priority": "high"
}

### Update task
PUT {{baseUrl}}/workflows/tasks/1
Content-Type: application/json

{
  "title": "Updated Task",
  "status": "completed"
}

### Delete task
DELETE {{baseUrl}}/workflows/tasks/1
```

{% hint style="info" %}
**Environment Variables**

Use variables in `.http` files to easily switch between environments:

* Development: `@baseUrl = https://localhost:5001`
* Staging: `@baseUrl = https://staging.example.com`
* Production: `@baseUrl = https://api.example.com`
  {% endhint %}

### Automated Testing with xUnit

Create integration tests for your workflows:

```csharp
public class TaskWorkflowTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public TaskWorkflowTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task GetTask_WithValidId_ReturnsTask()
    {
        // Act
        var response = await _client.GetAsync("/workflows/tasks/1");

        // Assert
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        Assert.Contains("\"id\":1", content);
    }

    [Fact]
    public async Task GetTask_WithInvalidId_ReturnsNotFound()
    {
        // Act
        var response = await _client.GetAsync("/workflows/tasks/999");

        // Assert
        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
    }

    [Fact]
    public async Task CreateTask_WithValidData_ReturnsCreated()
    {
        // Arrange
        var task = new { title = "Test Task", status = "active" };
        var content = new StringContent(
            JsonSerializer.Serialize(task),
            Encoding.UTF8,
            "application/json");

        // Act
        var response = await _client.PostAsync("/workflows/tasks", content);

        // Assert
        Assert.Equal(HttpStatusCode.Created, response.StatusCode);
        Assert.True(response.Headers.Contains("Location"));
    }
}
```

## Part 10: Debugging and Troubleshooting

### Using Elsa Studio for Debugging

1. **Navigate to Workflow Instances**: View all executions of your workflow
2. **Inspect Activity Execution**: See inputs/outputs for each activity
3. **Check Journal Entries**: View the execution timeline
4. **Review Variables**: Inspect variable values at each step

### Common Issues and Solutions

#### Issue: 404 Not Found

**Problem**: Workflow endpoint not responding

**Solutions**:

* Verify the workflow is **Published**
* Check that "Trigger Workflow" is enabled on HTTP Endpoint
* Ensure the path doesn't conflict with other routes
* Verify Elsa Server is running and configured correctly

#### Issue: Request Body is Null

**Problem**: Cannot read POST/PUT request body

**Solutions**:

* Set `Content-Type: application/json` header
* Ensure JSON is valid
* Use HTTP Endpoint's "Parsed Content" output
* Check that body isn't consumed elsewhere in the pipeline

#### Issue: Headers Not Available

**Problem**: Cannot read request headers

**Solutions**:

* Use HTTP Endpoint's "Headers" output variable
* Check header names are case-insensitive
* Verify headers are sent with the request

#### Issue: CORS Errors

**Problem**: Browser blocks requests from different origin

**Solutions**:

* Add CORS headers to Write HTTP Response activity
* Handle OPTIONS preflight requests
* Configure Elsa Server CORS policy

Example CORS workflow configuration:

**HTTP Endpoint Activity:**

```csharp
// Add support for OPTIONS method for CORS preflight
SupportedMethods = new[] { HttpMethods.Get, HttpMethods.Post, HttpMethods.Options }
```

**Write HTTP Response Activity:**

```csharp
// Always include CORS headers in production (with proper origin validation)
Headers = new Dictionary<string, string>
{
    ["Access-Control-Allow-Origin"] = "https://yourdomain.com",
    ["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS",
    ["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
}
```

### Enabling Detailed Logging

Configure logging in your Elsa Server's `appsettings.json`:

```json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Elsa": "Debug",
      "Elsa.Workflows": "Debug",
      "Elsa.Http": "Debug"
    }
  }
}
```

## Best Practices

### 1. Use Consistent Response Formats

Always return JSON in a consistent structure:

```json
// Success response
{
  "data": { /* resource */ },
  "timestamp": "2024-01-20T10:30:00Z"
}

// Error response
{
  "error": "Error message",
  "code": "ERROR_CODE",
  "timestamp": "2024-01-20T10:30:00Z",
  "details": []
}
```

### 2. Validate All Inputs

Never trust client input. Always validate:

* Required fields are present
* Data types are correct
* Values are within expected ranges
* Formats match requirements (email, URL, etc.)

### 3. Use Appropriate HTTP Methods

* **GET**: Retrieve resources (idempotent, no side effects)
* **POST**: Create resources (non-idempotent)
* **PUT**: Update entire resources (idempotent)
* **PATCH**: Partial updates (may be idempotent)
* **DELETE**: Remove resources (idempotent)

### 4. Return Proper Status Codes

Use semantic HTTP status codes to communicate results clearly.

### 5. Implement Security

* Validate authentication tokens
* Implement authorization checks
* Sanitize inputs to prevent injection attacks
* Use HTTPS in production
* Rate limit requests

### 6. Version Your APIs

Include version in the path:

* `/workflows/v1/tasks`
* `/workflows/v2/tasks`

Or use headers:

* `Accept: application/vnd.myapi.v1+json`

### 7. Document Your Endpoints

Provide clear documentation for each endpoint:

* Purpose and description
* Request format and parameters
* Response format and status codes
* Example requests and responses
* Error scenarios

### 8. Handle Timeouts

For long-running operations:

* Return 202 Accepted immediately
* Process asynchronously
* Provide status endpoint to check progress

### 9. Use Workflow Variables Wisely

* Name variables descriptively
* Choose appropriate storage (Workflow Instance vs Activity)
* Clean up large variables when no longer needed

### 10. Monitor and Log

* Log important events
* Track performance metrics
* Monitor error rates
* Set up alerts for critical issues

## Real-World Example: Complete Task API

Here's a complete workflow combining all the patterns we've learned:

### Workflow: Create Task with Full Validation

This workflow demonstrates:

* Request body parsing
* Comprehensive validation
* Authentication check
* Error handling
* Proper response codes
* Header management

**Variables:**

* `Headers` (ObjectDictionary)
* `RequestBody` (Object)
* `AuthToken` (string)
* `ValidationResult` (Object)
* `NewTask` (Object)
* `IsAuthenticated` (bool)

**Activities Flow:**

1. **HTTP Endpoint** (POST `/workflows/tasks`)
   * Outputs: Headers, RequestBody
2. **Extract Auth Token**
   * `AuthToken = Headers.Authorization`
3. **Validate Authentication**
   * Check if token is valid
   * Branch: Authenticated / Unauthorized
4. **Validate Request Body** (if authenticated)
   * Check required fields
   * Validate formats
   * Check business rules
5. **Decision: Valid Input?**
   * True: Create task
   * False: Return validation errors
6. **Create Task** (if valid)
   * Generate ID
   * Set timestamps
   * Prepare response
7. **Return Response**
   * 201 Created: With Location header
   * 400 Bad Request: With validation errors
   * 401 Unauthorized: If auth fails

## Summary

Congratulations! You've completed the comprehensive HTTP Workflows tutorial. You now know how to:

* ✅ Create RESTful endpoints for all HTTP methods (GET, POST, PUT, DELETE)
* ✅ Handle route parameters and query strings
* ✅ Parse and validate request bodies
* ✅ Read and set HTTP headers
* ✅ Implement proper error handling
* ✅ Return appropriate HTTP status codes
* ✅ Test workflows using various tools
* ✅ Debug and troubleshoot issues
* ✅ Apply best practices for production-ready APIs

### Next Steps

Now that you've mastered HTTP workflows, explore these advanced topics:

* [**External Application Interaction**](/guides/external-application-interaction): Integrate with external services
* [**Custom Activities**](/extensibility/custom-activities): Create reusable workflow components
* [**Authentication**](/guides/authentication): Secure your workflows
* [**Testing & Debugging**](/guides/testing-debugging): Advanced debugging techniques
* [**Distributed Hosting**](/hosting/distributed-hosting): Scale your workflows

### Resources

* [Elsa Workflows Documentation](/)
* [Expression Languages](/expressions/c)
* [Elsa GitHub Repository](https://github.com/elsa-workflows/elsa-core)
* [Community Discord](https://discord.gg/hhChk5H472)

### Feedback

Found an issue or have suggestions for improving this tutorial? Please [open an issue](https://github.com/elsa-workflows/elsa-gitbook/issues) on our GitHub repository.

Happy workflow building! 🚀


# Programmatic

## Before you start <a href="#before-you-start" id="before-you-start"></a>

For this guide, we will need the following:

* An [Elsa Server](https://elsa-workflows.github.io/elsa-documentation/elsa-server.html?section=Programmatic) project

Please return here when you are ready.

## Workflow Overview <a href="#workflow-overview" id="workflow-overview"></a>

We will define a new workflow called `GetUser`. The purpose of the workflow is to handle inbound HTTP requests by fetching a user by a given user ID from a backend API and writing them back to the client in JSON format.

For the backend API, we will use [reqres.in](https://reqres.in/), which returns fake data using real HTTP responses.

Our workflow will parse the inbound HTTP request by getting the desired user ID from a route parameter and use that value to make an API call to reqres.

The following is an example of such an HTTP request that you can try right now from your browser: <https://reqres.in/api/users/2>

The response should look similar to this:

```json
{
    "data": {
        "id": 2,
        "email": "janet.weaver@reqres.in",
        "first_name": "Janet",
        "last_name": "Weaver",
        "avatar": "https://reqres.in/img/faces/2-image.jpg"
    },
    "support": {
        "url": "https://reqres.in/#support-heading",
        "text": "To keep ReqRes free, contributions towards server costs are appreciated!"
    }
}
```

Our workflow will essentially be a proxy sitting in front of the reqres API and return a portion of the response.

## Create C# Workflow <a href="#create-workflow-using-csharp" id="create-workflow-using-csharp"></a>

Follow these steps to create the workflow from code.

{% stepper %}
{% step %}
Create Workflow

Create GetUser.cs and add the following code:

{% code title="GetUser.cs" %}

```csharp
using System.Dynamic;
using System.Net;
using Elsa.Http;
using Elsa.Http.Models;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Contracts;

namespace WorkflowApp.Web.Workflows;

public class GetUser : WorkflowBase
{
    protected override void Build(IWorkflowBuilder builder)
    {
        var routeDataVariable = builder.WithVariable<IDictionary<string, object>>();
        var userIdVariable = builder.WithVariable<string>();
        var userVariable = builder.WithVariable<ExpandoObject>();

        builder.Root = new Sequence
        {
            Activities =
            {
                new HttpEndpoint
                {
                    Path = new("users/{userid}"),
                    SupportedMethods = new(new[] { HttpMethods.Get }),
                    CanStartWorkflow = true,
                    RouteData = new(routeDataVariable)
                },
                new SetVariable
                {
                    Variable = userIdVariable,
                    Value = new(context =>
                    {
                        var routeData = routeDataVariable.Get(context)!;
                        var userId = routeData["userid"].ToString();
                        return userId;
                    })
                },
                new SendHttpRequest
                {
                    Url = new(context =>
                    {
                        var userId = userIdVariable.Get(context);
                        return new Uri($"https://reqres.in/api/users/{userId}");
                    }),
                    Method = new(HttpMethods.Get),
                    ParsedContent = new(userVariable),
                    ExpectedStatusCodes =
                    {
                        new HttpStatusCodeCase
                        {
                            StatusCode = StatusCodes.Status200OK,
                            Activity = new WriteHttpResponse
                            {
                                Content = new(context =>
                                {
                                    var user = (dynamic)userVariable.Get(context)!;
                                    return user.data;
                                }),
                                StatusCode = new(HttpStatusCode.OK)
                            }
                        },
                        new HttpStatusCodeCase
                        {
                            StatusCode = StatusCodes.Status404NotFound,
                            Activity = new WriteHttpResponse
                            {
                                Content = new("User not found"),
                                StatusCode = new(HttpStatusCode.NotFound)
                            }
                        }
                    }
                }
            }
        };
    }
}
```

{% endcode %}
{% endstep %}
{% endstepper %}

Let's go over this workflow section by section.

### Workflow Variables <a href="#workflow-variables" id="workflow-variables"></a>

```csharp
var routeDataVariable = builder.WithVariable<IDictionary<string, object>>();
var userIdVariable = builder.WithVariable<string>();
var userVariable = builder.WithVariable<ExpandoObject>();
```

Here, we defined 3 workflow variables.

The `routeDataVariable` variable is used to capture route data output from the HTTP endpoint activity. This variable is a dictionary.

The `userIdVariable` variable is used to store the user ID value that we get from the `routeDataVariable` dictionary.

The `userVariable` variable is used to capture the parsed response from the reqres API call. Since reqres returns JSON content and the capturing variable is of type `ExpandoObject`, the `SendHttpRequest` activity will parse the received JSON response into an `ExpandoObject`.

### HttpEndpoint Activity <a href="#httpendpoint-activity" id="httpendpoint-activity"></a>

```csharp
new HttpEndpoint
{
    Path = new("users/{userid}"),
    SupportedMethods = new(new[] { HttpMethods.Get }),
    CanStartWorkflow = true,
    RouteData = new(routeDataVariable)
},
```

Here we see the `HttpEndpoint` activity being defined and configured to be a trigger by setting `CanStartWorkflow` to `true`.

We set its `Path` property to respond to `users/{userid}`. Notice that we are using a route parameter using the name `userid`. This is the key we will use to grab the provided user ID from the inbound URL path.

To capture the route data, we assign the `routeDataVariable` variable to the `RouteData` output of the activity.

### SetVariable Activity <a href="#setvariable-activity" id="setvariable-activity"></a>

```csharp
new SetVariable
{
    Variable = userIdVariable,
    Value = new(context =>
    {
        var routeData = routeDataVariable.Get(context)!;
        var userId = routeData["userid"].ToString();
        return userId;
    })
},
```

Here we see the `SetVariable` activity defined and configured to set the `userIdVariable` variable to the dictionary entry with key `"userid"`.

We set its `Variable` property to reference the `userIdVariable` variable and its `Value` property to a callback that returns the received user ID from the route data dictionary.

### SendHttpRequest Activity <a href="#sendhttprequest-activity" id="sendhttprequest-activity"></a>

```csharp
new SendHttpRequest
{
    Url = new(context =>
    {
        var userId = userIdVariable.Get(context);
        return new Uri($"https://reqres.in/api/users/{userId}");
    }),
    Method = new(HttpMethods.Get),
    ParsedContent = new(userVariable),
    ExpectedStatusCodes =
    {
        new HttpStatusCodeCase
        {
            StatusCode = StatusCodes.Status200OK,
            Activity = new WriteHttpResponse
            {
                Content = new(context =>
                {
                    var user = (dynamic)userVariable.Get(context)!;
                    return user.data;
                }),
                StatusCode = new(HttpStatusCode.OK)
            }
        },
        new HttpStatusCodeCase
        {
            StatusCode = StatusCodes.Status404NotFound,
            Activity = new WriteHttpResponse
            {
                Content = new("User not found"),
                StatusCode = new(HttpStatusCode.NotFound)
            }
        }
    }
}
```

The `SendHttpRequest` activity is configured to send an HTTP request to the reqres API endpoint.

We set its `Url` property to a URL that includes the received user ID.

To capture the response, we assign its `ParsedContent` output to the `userVariable` variable.

Since the caller of the workflow might provide user IDs that don't correspond to a user record in the reqres backend, we configure the activity to handle two possible HTTP status codes:

* 200 OK
* 404 Not Found

For each of these possible status codes, we assign an appropriate `WriteHttpResponse` activity.

for the 200 case, the WriteHttpResponse activity access the `data` field of the user response object received from reqres:

```
new HttpStatusCodeCase
{
    StatusCode = StatusCodes.Status200OK,
    Activity = new WriteHttpResponse
    {
        Content = new(context =>
        {
            var user = (dynamic)userVariable.Get(context)!;
            return user.data;
        }),
        StatusCode = new(HttpStatusCode.OK)
    }
},
```

## Run Workflow <a href="#run-workflow" id="run-workflow"></a>

Since the workflow uses the `HttpEndpoint` activity, it will trigger when we send an HTTP request to the /workflows/users/{userId} path.

Try it out by navigating to <https://localhost:5001/workflows/users/2>.

The response should look similar to this:

```json
{
    "id": 2,
    "email": "janet.weaver@reqres.in",
    "first_name": "Janet",
    "last_name": "Weaver",
    "avatar": "https://reqres.in/img/faces/2-image.jpg"
}
```

### Summary <a href="#summary" id="summary"></a>

In this guide, we learned how to define a workflow from code.

We leveraged the `HttpEndpoint` activity and used is as a trigger to start the workflow.

The workflow is able to read route parameters and store it in a variable, which we then used as an input to send an API call to the reqres API that in turn returns the requested user.

We have also seen how to handle various responses from reqres: 200 OK and 404 Not Found

The source code for this guide can be found [here](https://github.com/elsa-workflows/elsa-guides/tree/main/src/guides/http-workflows).


# Designer

## Before you start <a href="#before-you-start" id="before-you-start"></a>

For this guide, we will need the following:

* An [Elsa Server](https://elsa-workflows.github.io/elsa-documentation/elsa-server.html?section=Designer) project
* An [Elsa Studio](https://elsa-workflows.github.io/elsa-documentation/docker.html?section=Designer#elsa-studio) instance

  ```bash
  docker pull elsaworkflows/elsa-studio-v3:latest
  docker run -t -i -e ASPNETCORE_ENVIRONMENT='Development' -e HTTP_PORTS=8080 -e ELSASERVER__URL=https://localhost:5001/elsa/api -p 14000:8080 elsaworkflows/elsa-studio-v3:latest
  ```

{% hint style="info" %}
**Port Numbers**

When starting Elsa Studio, make sure you provide it with the correct URL to the Elsa Server application.

For example, if Elsa Server runs on <https://localhost:5001>, the Docker command should look like this:

`docker run -t -i -e ASPNETCORE_ENVIRONMENT='Development' -e HTTP_PORTS=8080 -e ELSASERVER__URL=https://localhost:5001/elsa/api -p 14000:8080 elsaworkflows/elsa-studio-v3:latest`
{% endhint %}

Please return here when you are ready.

## Workflow Overview <a href="#workflow-overview" id="workflow-overview"></a>

We will define a new workflow called `GetUser`. The purpose of the workflow is to handle inbound HTTP requests by fetching a user by a given user ID from a backend API and writing them back to the client in JSON format.

For the backend API, we will use [JSONPlaceholder](https://jsonplaceholder.typicode.com/), which returns fake data using real HTTP responses.

Our workflow will parse the inbound HTTP request by getting the desired user ID from a route parameter and use that value to make an API call to JSONPlaceholder.

The following is an example of such an HTTP request that you can try right now from your browser: <https://jsonplaceholder.typicode.com/users/2>

The response should look similar to this:

```json
{
  "id": 2,
  "name": "Ervin Howell",
  "username": "Antonette",
  "email": "Shanna@melissa.tv",
  "address": {
    "street": "Victor Plains",
    "suite": "Suite 879",
    "city": "Wisokyburgh",
    "zipcode": "90566-7771",
    "geo": {
      "lat": "-43.9509",
      "lng": "-34.4618"
    }
  },
  "phone": "010-692-6593 x09125",
  "website": "anastasia.net",
  "company": {
    "name": "Deckow-Crist",
    "catchPhrase": "Proactive didactic contingency",
    "bs": "synergize scalable supply-chains"
  }
}
```

Our workflow will essentially be a proxy sitting in front of the JSONPlaceholder API and return the response.

## Designing the Workflow <a href="#create-workflow-using-designer" id="create-workflow-using-designer"></a>

Follow these steps to create the workflow using Elsa Studio.

{% stepper %}
{% step %}
**Create Get User Workflow**

Create a new workflow called Get User
{% endstep %}

{% step %}
**Add Activities**

Add and connect the following activities to the design surface:

* HTTP Endpoint
* Set Variable
* HTTP Request (flow)
* HTTP Response (for 200 OK)
* HTTP Response (for 404 Not Found)
  {% endstep %}

{% step %}
**Create Variables**

Create the following variables:

| Name          | Type             | Storage           |
| ------------- | ---------------- | ----------------- |
| RouteData     | ObjectDictionary | Workflow Instance |
| UserId        | string           | Workflow Instance |
| User          | Object           | Workflow Instance |
| {% endstep %} |                  |                   |

{% step %}
**Configure Activities**

Configure the activities as follows:

**HTTP Endpoint**

{% tabs %}
{% tab title="Input" %}

| Property          | Value            | Syntax  |
| ----------------- | ---------------- | ------- |
| Path              | `users/{userid}` | Default |
| Supported Methods | `Get`            | Default |
| {% endtab %}      |                  |         |

{% tab title="Output" %}

| Property     | Value     |
| ------------ | --------- |
| Route Data   | RouteData |
| {% endtab %} |           |

{% tab title="Common" %}

| Property         | Value   |
| ---------------- | ------- |
| Trigger Workflow | Checked |
| {% endtab %}     |         |
| {% endtabs %}    |         |

**Set Variable**

{% tabs %}
{% tab title="Input" %}

<table><thead><tr><th width="132">Property</th><th width="423">Value</th><th>Syntax</th></tr></thead><tbody><tr><td>Variable</td><td><code>UserId</code></td><td>Default</td></tr><tr><td>Value</td><td><code>{{ Variables.RouteData.userid }}</code></td><td>Liquid</td></tr></tbody></table>
{% endtab %}
{% endtabs %}

**HTTP Request (flow)**

{% tabs %}
{% tab title="Input" %}

<table><thead><tr><th width="100">Property</th><th width="458">Value</th><th width="100">Syntax</th></tr></thead><tbody><tr><td>Expected Status Codes</td><td><code>200, 404</code></td><td>Default</td></tr><tr><td>Url</td><td><code>return $"https://jsonplaceholder.typicode.com/users/{Variables.UserId}";</code></td><td>C#</td></tr><tr><td>Method</td><td><code>GET</code></td><td>Default</td></tr></tbody></table>
{% endtab %}

{% tab title="Output" %}

| Property       | Value  |
| -------------- | ------ |
| Parsed Content | `User` |
| {% endtab %}   |        |
| {% endtabs %}  |        |

**HTTP Response (200)**

{% tabs %}
{% tab title="Input" %}

| Property      | Value            | Syntax     |
| ------------- | ---------------- | ---------- |
| Status Code   | `OK`             | Default    |
| Content       | `variables.User` | JavaScript |
| {% endtab %}  |                  |            |
| {% endtabs %} |                  |            |

**HTTP Response (404)**

{% tabs %}
{% tab title="Input" %}

| Property      | Value            | Syntax  |
| ------------- | ---------------- | ------- |
| Status Code   | `NotFound`       | Default |
| Content       | `User not found` | Default |
| {% endtab %}  |                  |         |
| {% endtabs %} |                  |         |
| {% endstep %} |                  |         |

{% step %}
**Connect Activities**

Connect each activity to the next. Ensure that you connect the `200` and `404` outcomes of the HTTP Request (flow) activity to the appropriate HTTP Response activity.
{% endstep %}

{% step %}
**Publish**

Publish the workflow.
{% endstep %}
{% endstepper %}

The final result should look like this:

<figure><img src="/files/08beUO0Q9Kb9xn0Sk8Cc" alt=""><figcaption></figcaption></figure>

## Running the Workflow

Since the workflow uses the HTTP Endpoint activity, it will trigger when we send an HTTP request to the /workflows/users/{userId} path.

Try it out by navigating to <https://localhost:5001/workflows/users/2>.

The response should look similar to this:

```json
{
  "id": 2,
  "name": "Ervin Howell",
  "username": "Antonette",
  "email": "Shanna@melissa.tv",
  "address": {
    "street": "Victor Plains",
    "suite": "Suite 879",
    "city": "Wisokyburgh",
    "zipcode": "90566-7771",
    "geo": {
      "lat": "-43.9509",
      "lng": "-34.4618"
    }
  },
  "phone": "010-692-6593 x09125",
  "website": "anastasia.net",
  "company": {
    "name": "Deckow-Crist",
    "catchPhrase": "Proactive didactic contingency",
    "bs": "synergize scalable supply-chains"
  }
}
```

## Summary﻿

In this guide, we learned how to design a workflow using Elsa Studio.

We leveraged the `HttpEndpoint` activity and used is as a trigger to start the workflow.

The workflow is able to read route parameters and store it in a variable, which we then used as an input to send an API call to the JSONPlaceholder API that in turn returns the requested user.

We have also seen how to handle various responses from JSONPlaceholder: 200 OK and 404 Not Found

The workflow created in this guide can be found [here](https://raw.githubusercontent.com/elsa-workflows/elsa-guides/main/src/guides/http-workflows/Workflows/get-user.json).


# External Application Interaction

Configure Elsa 3.8.0 inbound webhook events and outbound webhook sinks, including custom activities, payloads, and endpoint security.

Elsa's Webhooks extension supports two different directions of communication:

* **Inbound events**: an external system posts a `WebhookEvent` to Elsa. A configured source maps the event to a trigger activity and resumes matching workflows.
* **Outbound events**: Elsa broadcasts events such as `Elsa.RunTask` to configured HTTP sinks. A separate application can receive the event and complete the work.

These paths share the `UseWebhooks` module, but they have different security and design concerns. Choose the inbound path when the external system is the source of a business event. Choose an outbound sink when Elsa is notifying another application that work should be performed.

This guide describes the implementation in the `release/3.8.0` snapshot of `elsa-extensions`.

## Install and enable the module

Add the Elsa HTTP Webhooks package that matches the rest of your Elsa 3.8.0 packages:

```bash
dotnet add package Elsa.Http.Webhooks --version 3.8.0
```

Enable it in the Elsa module configuration:

```csharp
using Elsa.Extensions;

builder.Services.AddElsa(elsa =>
{
    elsa.UseWebhooks();
});
```

`UseWebhooks` registers the WebhooksCore services, the dynamic activity provider, and the notification handlers used by both directions.

## Inbound webhook events

An inbound source describes the event types that an external system may send. Register sources through `WebhooksFeature.RegisterWebhookSource(s)` or the equivalent `IServiceCollection.RegisterWebhookSource(s)` extension. A source has a name and event types. An event type can include an `ActivityBinding` that provides the activity type name, display name, description, and payload type.

The binding is what makes an event appear as a usable activity in Studio. An event type without an activity binding can still be received and notified to the Webhooks extension, but it does not produce a browsable trigger activity.

The source object is supplied by WebhooksCore, so keep its definition in the integration package that owns the external event contract. The Elsa-side registration looks like this:

```csharp
using Elsa.Extensions;
using WebhooksCore;

var orderEvents = CreateOrderWebhookSource();

builder.Services.AddElsa(elsa =>
{
    elsa.UseWebhooks(webhooks =>
    {
        webhooks.RegisterWebhookSource(orderEvents);
    });
});
```

Replace `CreateOrderWebhookSource` with the factory from your integration. Do not invent event names in the workflow and assume they will match incoming requests: the endpoint matches the posted `eventType` against the registered source event types.

### What the runtime does

The released inbound path is:

1. `POST /webhooks` reads a JSON `WebhookEvent`.
2. Elsa finds the first registered source containing the posted `eventType`.
3. If no source matches, Elsa returns `200 OK` without starting a workflow.
4. For a match, Elsa sends a `WebhookEventReceived` notification.
5. The notification handler sends a stimulus whose activity type is derived from the source name and event type.
6. Matching trigger activities resume their workflows with the webhook event as workflow input.

The generated activity is based on `WebhookEventReceived`. The activity hides its internal event-type inputs from Studio and exposes the configured payload type as its output. At runtime it converts the received payload to that type when a payload type was configured.

The event's activity type is normally derived as:

```
Webhooks.{dehumanized source name}.{dehumanized event type}
```

When an `ActivityBinding.TypeName` is supplied, that explicit type name is used by the descriptor provider. Keep the type name stable after workflows have been saved; changing it makes existing workflow definitions unable to resolve the trigger activity.

There is an important `release/3.8.0` compatibility detail: the inbound stimulus handler derives the stimulus type from the source and event names, while the descriptor provider uses `ActivityBinding.TypeName`. Keep an explicit binding type name aligned with the derived `Webhooks.{dehumanized source}.{dehumanized event}` value, or the designer may show an activity that does not receive the matching stimulus.

An inbound request has the following shape:

```http
POST /webhooks
Content-Type: application/json

{
  "eventType": "Order.Approved",
  "payload": {
    "orderId": "order-123",
    "approvedBy": "alice@example.com"
  }
}
```

The exact payload schema belongs to the external integration. Elsa matches the event type; it does not authenticate, validate a provider-specific signature, or decide whether an unknown event should be a client error.

## Secure the inbound endpoint

In `release/3.8.0`, the Webhooks extension maps `POST /webhooks` with `AllowAnonymous()`. This is deliberate for a generic webhook receiver, but it means the extension does not protect the endpoint for you.

Put authentication and request validation in front of the endpoint or in a host-level endpoint filter/middleware. At minimum, validate:

* the provider signature or shared secret;
* timestamp and replay limits;
* the request body and content type;
* the event type and payload schema; and
* tenant or source identity before a workflow is resumed.

Also consider rate limiting, maximum body size, idempotency keys, and a dedicated ingress route. Do not expose the anonymous endpoint directly to the public internet without an application-owned verification layer. Elsa's normal API permissions do not turn this anonymous route into a signed webhook receiver.

Because an unknown event returns `200 OK`, monitor rejected or unmatched events in the validation layer rather than relying on the Elsa endpoint status alone.

## Outbound webhook sinks

An outbound sink broadcasts an Elsa webhook event to another HTTP endpoint. Register a simple endpoint in code:

```csharp
using Elsa.Extensions;

builder.Services.AddElsa(elsa =>
{
    elsa.UseWebhooks(webhooks =>
    {
        webhooks.RegisterWebhookSink(
            new Uri("https://onboarding.example.com/api/webhooks"));
    });
});
```

For a fully configured `WebhookSink`, use `RegisterSink` or bind `WebhookSinksOptions` from configuration. Use the sink's event filters to limit which event types are delivered. Keep the endpoint private or protect it with the authentication and signature scheme expected by the receiving application.

`RunTaskHandler` is the built-in outbound example. When a `RunTask` activity executes, it broadcasts an `Elsa.RunTask` event whose payload includes:

* workflow instance ID;
* workflow definition ID and name;
* tenant ID and correlation ID;
* task ID and task name; and
* the task payload.

The receiving application should acknowledge quickly, authenticate the request, persist the task idempotently, and perform long-running work outside the request if necessary. The webhook is a notification path; it is not a distributed transaction or a replacement for a durable task queue.

## Workflow design with inbound events

For an approval callback or external business event:

1. register a source event with a stable `eventType` and activity binding;
2. in Studio, add the generated webhook trigger activity;
3. use its typed payload output in the following activities;
4. publish the workflow; and
5. send a signed test request through the protected ingress route.

The trigger activity creates a bookmark when the workflow is not being started by its trigger. When the matching stimulus arrives, Elsa resumes the waiting workflow and sets the payload output. This makes webhook events suitable for long-running workflows that wait for an external decision.

If a source has multiple event types, give each event an intentional activity binding and payload type. Avoid using `object` unless the workflow really needs untyped payloads; a concrete payload type gives Studio and the runtime a more useful contract.

## Troubleshooting

### The generated activity is missing from Studio

* Confirm `UseWebhooks` is enabled in the Elsa Server, not only in Studio.
* Confirm the source is registered and the event type has an `ActivityBinding`.
* Refresh the server activity registry after changing source configuration.
* Check that the binding's type name is stable and that the server can resolve the payload type.

### The request returns `200 OK` but no workflow starts

* Check the exact case and spelling of `eventType`.
* Confirm the matching source is registered in the server process receiving the request.
* Confirm the workflow is published and uses the generated activity for the same source/event binding.
* Inspect the ingress validation layer and the workflow trigger/journal logs.

An unknown event also returns `200 OK` in the released endpoint, so do not use that response alone as proof that a workflow was resumed.

### A payload is present but has the wrong type

Check the event's configured `PayloadType` and the JSON shape sent by the external system. The activity converts the payload to that type when a type is configured; conversion errors must be handled by the host's normal request and workflow error policies.

### The outbound application does not receive `RunTask`

Confirm that:

* the sink is registered and its event filter includes `Elsa.RunTask`;
* the receiver URL is reachable from the Elsa Server process;
* the receiver accepts the payload and authentication scheme; and
* the receiving application is not treating retries as new tasks.

## Release source

This page was checked against `release/3.8.0` in `elsa-extensions` at [`d407e962`](https://github.com/elsa-workflows/elsa-extensions/tree/d407e9621770a55427ac6c2315bd779da08d5fea):

* [`UseWebhooks` and `WebhooksFeature`](https://github.com/elsa-workflows/elsa-extensions/blob/d407e9621770a55427ac6c2315bd779da08d5fea/src/modules/http/Elsa.Http.Webhooks/Extensions/ModuleExtensions.cs)
* [`WebhooksFeature`](https://github.com/elsa-workflows/elsa-extensions/blob/d407e9621770a55427ac6c2315bd779da08d5fea/src/modules/http/Elsa.Http.Webhooks/Features/WebhooksFeature.cs)
* [`WebhookEventActivityProvider`](https://github.com/elsa-workflows/elsa-extensions/blob/d407e9621770a55427ac6c2315bd779da08d5fea/src/modules/http/Elsa.Http.Webhooks/ActivityProviders/WebhookEventActivityProvider.cs)
* [`POST /webhooks`](https://github.com/elsa-workflows/elsa-extensions/blob/d407e9621770a55427ac6c2315bd779da08d5fea/src/modules/http/Elsa.Http.Webhooks/Endpoints/Webhooks/Endpoint.cs)
* [`WebhookEventReceived` activity](https://github.com/elsa-workflows/elsa-extensions/blob/d407e9621770a55427ac6c2315bd779da08d5fea/src/modules/http/Elsa.Http.Webhooks/Activities/WebhookEventReceived.cs)
* [`InvokeWebhookActivities`](https://github.com/elsa-workflows/elsa-extensions/blob/d407e9621770a55427ac6c2315bd779da08d5fea/src/modules/http/Elsa.Http.Webhooks/Handlers/InvokeWebhookActivities.cs)
* [`RunTaskHandler`](https://github.com/elsa-workflows/elsa-extensions/blob/d407e9621770a55427ac6c2315bd779da08d5fea/src/modules/http/Elsa.Http.Webhooks/Handlers/RunTaskHandler.cs)


# Loading Workflows from JSON

Loading workflows from JSON is a great way to store workflows in a database or file system. This guide will show you how to load workflows from JSON files.

## Console application <a href="#console-application" id="console-application"></a>

The most straightforward way to load workflows from JSON files is to simply load the contents of a JSON file, deserialise it and then execute the deserialised workflow.

{% stepper %}
{% step %}

#### Create Console Project

```bash
dotnet new console -n "ElsaConsole" -f net8.0
cd ElsaConsole
dotnet add package Elsa
dotnet add package Elsa.Testing.Shared.Integration
```

{% endstep %}

{% step %}

#### Update Program.cs

Here's a complete Program.cs file that demonstrates how to load a workflow from a JSON file and execute it:

{% code title="Program.cs" %}

```csharp
using Elsa.Extensions;
using Elsa.Testing.Shared;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Models;
using Microsoft.Extensions.DependencyInjection;

// Setup service container.
var services = new ServiceCollection();

// Add Elsa services.
services.AddElsa();

// Build service container.
var serviceProvider = services.BuildServiceProvider();

// Populate registries. This is only necessary for applications  that are not using hosted services.
await serviceProvider.PopulateRegistriesAsync();

// Import a workflow from a JSON file.
var workflowJson = await File.ReadAllTextAsync("HelloWorld.json");

// Get a serializer to deserialize the workflow.
var serializer = serviceProvider.GetRequiredService<IActivitySerializer>();

// Deserialize the workflow model.
var workflowDefinitionModel = serializer.Deserialize<WorkflowDefinitionModel>(workflowJson);

// Map the model to a Workflow object.
var workflowDefinitionMapper = serviceProvider.GetRequiredService<WorkflowDefinitionMapper>();
var workflow = workflowDefinitionMapper.Map(workflowDefinitionModel);

// Get a workflow runner to run the workflow.
var workflowRunner = serviceProvider.GetRequiredService<IWorkflowRunner>();

// Run the workflow.
await workflowRunner.RunAsync(workflow);
```

{% endcode %}
{% endstep %}

{% step %}

#### Create Workflow JSON file

Create a new file called HelloWorld.json in the root of the project and make sure it is configured to be copied to the output directory.

{% code title="HelloWorld.json" %}

```json
{
  "id": "HelloWorld-v1",
  "definitionId": "HelloWorld",
  "name": "Hello World",
  "isLatest": true,
  "isPublished": true,
  "root": {
    "id": "Flowchart1",
    "type": "Elsa.Flowchart",
    "activities": [
      {
        "id": "WriteLine1",
        "type": "Elsa.WriteLine",
        "text": {
          "typeName": "String",
          "expression": {
            "type": "Literal",
            "value": "Hello World!"
          }
        }
      }
    ]
  }
}
```

{% endcode %}
{% endstep %}

{% step %}

#### Run the Program

Run the program:

```bash
dotnet run
```

{% endstep %}
{% endstepper %}

The console should output the following:

```
Hello World!
```

## Elsa Server

When you're hosting an [Elsa Server](/application-types/elsa-server), providing workflows from JSON files is even easier.

All you need to do is create a folder called *Workflows* and add any number of workflow JSON files to it.

Let's try it out:

{% stepper %}
{% step %}

#### Setup Elsa Server

Setup an [Elsa Server](/application-types/elsa-server) project.
{% endstep %}

{% step %}

#### Create Workflows Folder

Create a new folder called *Workflows*.
{% endstep %}

{% step %}

#### Create Workflow JSON File

Create a new file called *HelloWorld.json* in the root of the project and make sure it is configured to be copied to the output directory.

{% code title="HelloWorld.json" %}

```json
{
  "id": "HelloWorld-v1",
  "definitionId": "HelloWorld",
  "name": "Hello World",
  "isLatest": true,
  "isPublished": true,
  "root": {
    "id": "Flowchart1",
    "type": "Elsa.Flowchart",
    "activities": [
      {
        "id": "WriteLine1",
        "type": "Elsa.WriteLine",
        "text": {
          "typeName": "String",
          "expression": {
            "type": "Literal",
            "value": "Hello World!"
          }
        }
      }
    ]
  }
}
```

{% endcode %}
{% endstep %}

{% step %}

#### Run the Program

Run the program:

```bash
dotnet run --urls "https://localhost:5001"
```

{% endstep %}

{% step %}

#### Run the Workflow

Run the workflow using the following curl:

```bash
curl --location --request POST 'https://localhost:5001/elsa/api/workflow-definitions/HelloWorld/execute' \
--header 'Authorization: ApiKey {your-api-key}'
```

Alternatively, [start an Elsa Studio container](/getting-started/containers/docker#elsa-studio) and run the workflow from there.
{% endstep %}
{% endstepper %}

## Loading Workflows from Blob Storage

If you need to load workflows from cloud blob storage (Azure Blob Storage, AWS S3, etc.), Elsa provides a dedicated workflow provider package:

```bash
dotnet add package Elsa.WorkflowProviders.BlobStorage
```

> **Note:** Earlier documentation may have incorrectly referenced `Elsa.WorkflowProviders.FluentStorage`. The correct package name is `Elsa.WorkflowProviders.BlobStorage`.

For provider behavior, reload operations, and custom external sources, see [Workflow Providers](/extensibility/workflow-providers).

## Summary <a href="#summary" id="summary"></a>

In this guide, we've demonstrated configuring an Elsa Server to host workflows from JSON files. We covered loading a JSON file, deserialising it into the `Workflow` class, and executing the workflow.


# Plugins & Modules

Complete guide to extending Elsa Workflows with custom modules, features, and activities. Learn how to create reusable plugins and distribute them as NuGet packages.

Elsa Workflows provides a powerful and flexible extensibility system that allows you to create custom modules, features, and activities tailored to your specific needs. This guide will teach you how to extend Elsa with your own functionality and package it for reuse across projects.

## Overview

The Elsa extensibility model is built around three core concepts:

* **Modules**: Containers that group related features and provide a unified configuration interface
* **Features**: Self-contained units of functionality that register services, activities, and other components
* **Activities**: The building blocks of workflows that encapsulate specific actions or operations

This architecture enables you to:

* Create domain-specific activities that encapsulate business logic
* Package and distribute reusable extensions as NuGet packages
* Maintain clean separation of concerns in large applications
* Configure complex functionality through simple, fluent APIs

For independently deployed, server-side extension artifacts, see the [DropIns guide](/guides/modules-and-plugins/dropins). DropIns are discovered from a trusted directory and have a distinct install/configure lifecycle; they are not the same as normal compile-time module registration.

## Table of Contents

* [Key Concepts](#key-concepts)
  * [Modules & Features](#modules--features)
  * [Activity Discovery & Registration](#activity-discovery--registration)
* [Creating a Custom Feature](#creating-a-custom-feature)
  * [Step 1: Define Your Feature Class](#step-1-define-your-feature-class)
  * [Step 2: Configure Services](#step-2-configure-services)
  * [Step 3: Create Extension Methods](#step-3-create-extension-methods)
  * [Step 4: Register Your Feature](#step-4-register-your-feature)
* [Creating Custom Activities](#creating-custom-activities)
  * [Basic Activity Structure](#basic-activity-structure)
  * [Defining Inputs and Outputs](#defining-inputs-and-outputs)
  * [Activity Attributes](#activity-attributes)
  * [Registering Activities](#registering-activities)
* [Packaging & Distribution](#packaging--distribution)
  * [Package manifests for extensions](/guides/plugins-modules/package-manifests)
* [Runtime DropIns](/guides/modules-and-plugins/dropins)
* [Advanced Topics](#advanced-topics)
* [Complete Examples](#complete-examples)

## Key Concepts

### Modules & Features

Elsa uses a hierarchical configuration system where **modules** contain **features**, and features register the actual services and components.

#### IModule Interface

The `IModule` interface represents a container for features. The Elsa configuration system provides a default module implementation that you'll typically work with through extension methods.

#### FeatureBase Class

`FeatureBase` is the base class for all features in Elsa. A feature:

* Encapsulates related functionality
* Registers services with dependency injection
* Can configure workflow options
* Follows a two-phase initialization: `Configure()` and `Apply()`

**Lifecycle Methods:**

1. **Configure()**: Called during application startup to register services and configure options. This is where you:
   * Register activities using `AddActivitiesFrom<T>()`
   * Register workflows using `AddWorkflowsFrom<T>()`
   * Add custom services to the DI container
   * Configure workflow options
2. **Apply()**: Called after all features have been configured. Use this for:
   * Post-configuration tasks that depend on other features
   * Final validation
   * Complex initialization logic

#### UseXyz() Pattern

Elsa follows a convention where features are enabled using `UseXyz()` extension methods. This pattern:

* Provides a fluent, discoverable API
* Allows optional configuration via lambda expressions
* Returns `IModule` for method chaining

Example:

```csharp
builder.Services.AddElsa(elsa => elsa
    .UseMyFeature()
    .UseAnotherFeature(feature =>
    {
        // Configure the feature
    })
);
```

### Activity Discovery & Registration

Elsa provides several methods for registering activities and workflows:

#### AddActivitiesFrom()

Scans the assembly containing type `T` and registers all classes marked with the `[Activity]` attribute:

```csharp
Module.AddActivitiesFrom<MyFeature>();
```

This method:

* Discovers all activity classes in the assembly
* Registers them with the activity registry
* Makes them available in the workflow designer

#### AddWorkflowsFrom()

Similar to `AddActivitiesFrom<T>()`, but registers workflow definitions:

```csharp
Module.AddWorkflowsFrom<MyFeature>();
```

If the activity list is generated from runtime or external data, use an [`IActivityProvider`](/extensibility/activity-type-providers) instead of scanning a fixed assembly.

## Creating a Custom Feature

Let's walk through creating a custom feature step by step.

### Step 1: Define Your Feature Class

Create a class that inherits from `FeatureBase`:

```csharp
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Microsoft.Extensions.DependencyInjection;

namespace MyWorkflows.Features;

public class MyFeature : FeatureBase
{
    public MyFeature(IModule module) : base(module)
    {
    }

    public override void Configure()
    {
        // Configuration goes here
    }

    public override void Apply()
    {
        // Post-configuration goes here (optional)
    }
}
```

**Key Points:**

* The constructor must accept `IModule` and pass it to the base class
* Override `Configure()` to register services and components
* Override `Apply()` only if you need post-configuration logic

### Step 2: Configure Services

Inside the `Configure()` method, register your activities and services:

```csharp
public override void Configure()
{
    // Register activities from this assembly
    Module.AddActivitiesFrom<MyFeature>();
    
    // Register custom services
    Services.AddSingleton<IMyCustomService, MyCustomService>();
    Services.AddScoped<IMyRepository, MyRepository>();
    
    // Studio-side UI hint handlers are registered in the Studio application
    // with services.AddUIHintHandler<T>().
}
```

**Available Registration Methods:**

* `Module.AddActivitiesFrom<T>()`: Register all activities in the assembly
* `Module.AddWorkflowsFrom<T>()`: Register all workflow definitions in the assembly
* `Services.Add...()`: Access the service collection directly for custom registrations
* `Module.ConfigureWorkflowOptions()`: Configure workflow-specific settings

### Step 3: Create Extension Methods

Create a static extension class with `UseXyz()` methods following the Elsa convention:

```csharp
using Elsa.Features.Services;
using MyWorkflows.Features;

namespace MyWorkflows.Extensions;

public static class ModuleExtensions
{
    public static IModule UseMyFeature(
        this IModule module, 
        Action<MyFeature>? configure = null)
    {
        module.Use(configure);
        return module;
    }
}
```

**Pattern with Options:**

For more complex configuration, use an options class:

```csharp
public static class ModuleExtensions
{
    public static IModule UseMyFeature(
        this IModule module, 
        Action<MyFeatureOptions>? configure = null)
    {
        return module.Use<MyFeature>(feature =>
        {
            if (configure != null)
            {
                var options = new MyFeatureOptions();
                configure(options);
                
                // Apply options to feature properties or services
                if (options.EnableAdvancedFeatures)
                {
                    feature.Services.AddSingleton<IAdvancedService, AdvancedService>();
                }
            }
        });
    }
}

public class MyFeatureOptions
{
    public bool EnableAdvancedFeatures { get; set; } = false;
    public string? ApiKey { get; set; }
    public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30);
}
```

### Step 4: Register Your Feature

In your application's `Program.cs` or `Startup.cs`, use your extension method:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddElsa(elsa => elsa
    .UseMyFeature()
    // Or with configuration:
    .UseMyFeature(options =>
    {
        options.EnableAdvancedFeatures = true;
        options.ApiKey = builder.Configuration["MyFeature:ApiKey"];
    })
);
```

## Creating Custom Activities

Custom activities are the primary way to extend workflow functionality. See [examples/SampleActivity.cs](https://github.com/elsa-workflows/elsa-gitbook/tree/main/guides/plugins-modules/examples/SampleActivity.cs) for a complete example.

### Basic Activity Structure

Activities inherit from `CodeActivity` or `CodeActivity<T>` (for activities with outputs):

```csharp
using Elsa.Workflows;
using Elsa.Workflows.Attributes;

[Activity("MyWorkflows", "Sample", "Description of what this activity does")]
public class SampleActivity : CodeActivity<string>
{
    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
    {
        // Activity logic here
        
        // Set output if using CodeActivity<T>
        context.Set(Result, "output value");
        
        // Complete the activity
        await context.CompleteActivityAsync();
    }
}
```

**Base Classes:**

* `CodeActivity`: For activities without a return value
* `CodeActivity<T>`: For activities that produce a single output of type `T`
* `Activity`: For more complex activities with custom behavior

### Defining Inputs and Outputs

Use the `[Input]` and `[Output]` attributes to define activity ports:

```csharp
[Activity("MyWorkflows", "Data", "Processes a message with optional prefix")]
public class ProcessMessage : CodeActivity<string>
{
    [Input(Description = "The message to process")]
    public Input<string> Message { get; set; } = default!;

    [Input(
        Description = "Optional prefix to prepend", 
        DefaultValue = "INFO")]
    public Input<string?> Prefix { get; set; } = default!;

    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
    {
        var message = context.Get(Message);
        var prefix = context.Get(Prefix);
        
        var result = $"{prefix}: {message}";
        
        context.Set(Result, result);
        await context.CompleteActivityAsync();
    }
}
```

**Input/Output Features:**

* `Description`: Shown in the workflow designer
* `DefaultValue`: Default value if not specified
* `UIHint`: Custom UI control for the property editor
* `Category`: Groups related properties in the designer

### Activity Attributes

The `[Activity]` attribute configures how the activity appears in the designer:

```csharp
[Activity(
    Namespace = "MyCompany.MyProduct",    // Logical grouping
    Category = "Integration",              // Designer category
    Description = "Detailed description", // Shown in tooltips
    DisplayName = "My Activity"           // Display name (optional)
)]
public class MyActivity : CodeActivity
{
    // ...
}
```

**Attribute Parameters:**

* **Namespace**: Groups activities logically (e.g., "MyCompany.Integration")
* **Category**: Organizes activities in the designer toolbox
* **Description**: Provides help text for workflow designers
* **DisplayName**: Overrides the class name in the designer

### Registering Activities

Activities are registered via features:

```csharp
public override void Configure()
{
    // Registers all activities in the assembly containing MyFeature
    Module.AddActivitiesFrom<MyFeature>();
}
```

This scans for all types marked with `[Activity]` and registers them with the activity registry. They become immediately available in:

* The workflow designer
* Programmatic workflow definitions
* The workflow execution engine

## Packaging & Distribution

For the release-backed package metadata contract, see [Package manifests for extensions](/guides/plugins-modules/package-manifests). It covers runtime compatibility, infrastructure requirements, deploy-time settings, and the build/pack checks needed before publishing a NuGet extension.

To share your custom modules, package them as NuGet packages:

### 1. Create a Class Library Project

```bash
dotnet new classlib -n MyWorkflows.Extensions
cd MyWorkflows.Extensions
dotnet add package Elsa
dotnet add package Elsa.Workflows.Core
```

### 2. Organize Your Code

```
MyWorkflows.Extensions/
├── Activities/
│   ├── SampleActivity.cs
│   └── AnotherActivity.cs
├── Features/
│   └── MyFeature.cs
├── Extensions/
│   └── ModuleExtensions.cs
└── MyWorkflows.Extensions.csproj
```

### 3. Configure the .csproj File

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <PackageId>MyCompany.MyWorkflows.Extensions</PackageId>
    <Version>1.0.0</Version>
    <Authors>Your Name</Authors>
    <Description>Custom Elsa Workflows extensions</Description>
    <PackageTags>elsa;workflows;extensions</PackageTags>
    <RepositoryUrl>https://github.com/yourorg/yourrepo</RepositoryUrl>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Elsa" Version="3.7.0" />
    <PackageReference Include="Elsa.Workflows.Core" Version="3.7.0" />
  </ItemGroup>
</Project>
```

### 4. Build and Publish

```bash
dotnet pack -c Release
dotnet nuget push bin/Release/MyCompany.MyWorkflows.Extensions.1.0.0.nupkg \
  --api-key YOUR_API_KEY \
  --source https://api.nuget.org/v3/index.json
```

### 5. Consume the Package

Users can then install and use your package:

```bash
dotnet add package MyCompany.MyWorkflows.Extensions
```

```csharp
builder.Services.AddElsa(elsa => elsa
    .UseMyFeature()
);
```

## Advanced Topics

### Custom UI Hint Handlers

UI hint handlers control how activity properties are edited in the workflow designer:

```csharp
public class MyCustomUIHintHandler : IUIHintHandler
{
    public string UISyntax => "MyCustomHint";

    public bool GetSupportsUIHint(string uiHint) => uiHint == UISyntax;

    public RenderFragment DisplayInputEditor(DisplayInputEditorContext context) =>
        builder => builder.AddContent(0, "Render a custom editor here.");
}
```

Register in your Studio application:

```csharp
services.AddUIHintHandler<MyCustomUIHintHandler>();
```

### Custom Serializers

For complex data types, implement custom serializers:

```csharp
public class MyTypeSerializer : ISerializer
{
    public object Deserialize(string data)
    {
        // Deserialization logic
    }

    public string Serialize(object obj)
    {
        // Serialization logic
    }
}
```

Register in your feature:

```csharp
Services.AddSingleton<ISerializer, MyTypeSerializer>();
```

### Activity Execution Context

The `ActivityExecutionContext` provides access to:

* **Workflow Instance**: Current workflow state and variables
* **Input/Output**: Get and set activity inputs and outputs
* **Journal**: Log custom data for debugging
* **Cancellation**: Handle workflow cancellation
* **Services**: Access dependency injection container

```csharp
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
    // Access workflow variables
    var workflowVar = context.GetWorkflowVariable<string>("MyVar");
    
    // Access services
    var myService = context.GetRequiredService<IMyService>();
    
    // Log to journal
    context.JournalData.Add("CustomKey", "CustomValue");
    
    // Check cancellation
    if (context.CancellationToken.IsCancellationRequested)
        return;
    
    // ... activity logic
}
```

## Complete Examples

For complete, working examples, see:

* [SampleActivity.cs](https://github.com/elsa-workflows/elsa-gitbook/tree/main/guides/plugins-modules/examples/SampleActivity.cs) - A full custom activity with inputs and outputs
* [MyFeature.cs](https://github.com/elsa-workflows/elsa-gitbook/tree/main/guides/plugins-modules/examples/MyFeature.cs) - A complete feature implementation
* [ModuleExtensions.cs](https://github.com/elsa-workflows/elsa-gitbook/tree/main/guides/plugins-modules/examples/ModuleExtensions.cs) - Extension methods following Elsa conventions

## Best Practices

1. **Follow Naming Conventions**
   * Use `UseXyz()` for feature extension methods
   * Name features as `XyzFeature`
   * Use clear, descriptive activity names
2. **Provide Good Metadata**
   * Use descriptive `[Activity]` attributes
   * Add meaningful descriptions to inputs/outputs
   * Include usage examples in XML comments
3. **Handle Errors Gracefully**
   * Validate inputs in activities
   * Provide helpful error messages
   * Consider retry logic for transient failures
4. **Test Thoroughly**
   * Unit test activities independently
   * Integration test features
   * Test with the workflow designer
5. **Document Your Extensions**
   * Include XML documentation comments
   * Provide usage examples
   * Document configuration options

## Further Reading

* [Custom Activities Guide](/extensibility/custom-activities) - Detailed activity creation guide
* [Elsa Core Repository](https://github.com/elsa-workflows/elsa-core) - Official source code and examples
* [Alterations Feature Guide](/features/alterations) - Built-in alterations reference

## Support

For the current support-channel map, release-checking guidance, and question checklist, see [Community & Resources](/guides/community-resources).


# Package Manifests for Extensions

Explain the Elsa 3.8 package manifest generated for CShells extension packages, including runtime compatibility, infrastructure requirements, and deploy-time settings.

Use this guide when you publish a NuGet package that contains Elsa server extensions implemented as CShells features. Elsa 3.8 can generate an `elsa-package.json` manifest while the package is built and include it at the root of the NuGet package. The manifest describes what the package contains and what an operator needs to configure; it does not replace your feature's service registration or provision infrastructure.

## What the manifest is for

The package manifest is build-time metadata for extension packages. It helps package and runtime tooling answer questions such as:

* Is this package intended for Elsa Server or Elsa Studio?
* Does a feature need a database, cache, message broker, or another resource?
* Which deploy-time settings should an operator provide?
* Is a setting secret, advanced, experimental, or restart-sensitive?

The manifest is not an application configuration file. `ManifestInfrastructure` and `ManifestSetting` do not create a database, bind options, add a health check, restart a shell, or make a setting available in Studio automatically. Your feature still has to register services and read its configuration.

## How release packages generate it

The release source uses `Elsa.Platform.PackageManifest.Generator` as a private build dependency. Core imports the shared package-manifest MSBuild props for projects that contain a `ShellFeatures` directory; extension modules apply the same generator package and compile the runtime-kind hint file from their modules build props. The generator creates the manifest during build/pack, and the default package path is `elsa-package.json`.

The release source uses different generator patch versions in the two repositories (`0.0.1-preview.53` in Core and `0.0.1-preview.50` in Extensions). Keep the generator version aligned with the Elsa release and inspect the generated manifest before publishing.

If your extension project is not already covered by a shared build props file, add the generator as a private build dependency. This is the pattern used by the released Extensions modules:

```xml
<PackageReference
    Include="Elsa.Platform.PackageManifest.Generator"
    Version="0.0.1-preview.50"
    PrivateAssets="all" />
```

Use the generator version selected by the release you target; Core 3.8.0 uses `0.0.1-preview.53`.

Relevant release files:

* [Core package-manifest MSBuild props](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/src/PackageManifest.props)
* [Core package-manifest runtime hint](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/src/PackageManifestHints.cs)
* [Extensions module build props](https://github.com/elsa-workflows/elsa-extensions/blob/d407e9621770a55427ac6c2315bd779da08d5fea/src/modules/Directory.Build.props)
* [Extensions package-manifest runtime hint](https://github.com/elsa-workflows/elsa-extensions/blob/d407e9621770a55427ac6c2315bd779da08d5fea/src/modules/PackageManifestHints.cs)
* [Extensions MassTransit Azure Service Bus feature](https://github.com/elsa-workflows/elsa-extensions/blob/d407e9621770a55427ac6c2315bd779da08d5fea/src/modules/servicebus/Elsa.ServiceBus.MassTransit.AzureServiceBus/ShellFeatures/MassTransitAzureServiceBusFeature.cs)

## Declare runtime compatibility

Mark the package at assembly level when all of its features target one runtime kind:

```csharp
using Elsa.Platform.PackageManifest.Generator.Hints;

[assembly: ManifestRuntimeKind(ElsaRuntimeKinds.Server)]
```

The released Core and Extensions sources both use `ElsaRuntimeKinds.Server`, which resolves to `elsa.server`. The generator also supports feature-level runtime hints when only part of a package is runtime-specific. Use `ElsaRuntimeKinds.Studio` for Studio features, and do not label a package as Studio-compatible merely because it contributes workflow activities that appear in a Studio-hosted designer.

## Describe infrastructure requirements

Attach one or more infrastructure requirements to the feature that uses the resource. For example, the released Azure Service Bus feature declares the provider, a stable identifier, the infrastructure kind, and related configuration keys:

```csharp
[ManifestInfrastructure(
    "azure-service-bus",
    "service-bus",
    Reason = "Publishes and consumes workflow messages through Azure Service Bus.",
    Providers = new[] { "Azure Service Bus" },
    ConfigurationKeys = new[] { "ConnectionStringOrName" })]
public class AzureServiceBusShellFeature : IShellFeature
{
    // The feature still binds and uses this setting in ConfigureServices.
}
```

The first argument is the requirement identifier and the second is its kind. The attribute also supports `Optional`, `Reason`, `Capabilities`, `Providers`, `ConfigurationKeys`, and `Extensions`. Use the metadata to explain the dependency to tooling and operators; it is not a provisioning declaration.

Examples from the release use `database` for Quartz and persistence stores, `message-broker` for RabbitMQ and Kafka, and `service-bus` for Azure Service Bus. The same logical infrastructure can be declared by multiple features; give each requirement an identifier that is stable and meaningful within the package.

A generated manifest contains package and feature records. The following fragment shows the fields relevant to infrastructure and settings; inspect the file produced for your package for the complete schema and inferred metadata:

```json
{
  "schemaVersion": "1.0",
  "package": {
    "id": "MyCompany.Elsa.Messaging",
    "version": "1.0.0"
  },
  "features": [
    {
      "id": "MyCompany.Elsa.Messaging.AzureServiceBus",
      "settings": [
        {
          "name": "ConnectionStringOrName",
          "required": true,
          "secret": true,
          "restartRequired": true
        }
      ],
      "infrastructure": [
        {
          "id": "azure-service-bus",
          "kind": "service-bus",
          "providers": ["Azure Service Bus"]
        }
      ]
    }
  ]
}
```

## Describe deploy-time settings

Use `ManifestSetting` on a public, settable feature property when the property is a deploy-time setting. The released Azure Service Bus feature marks its connection value as required, secret, and restart-sensitive:

```csharp
[ManifestSetting(
    DisplayName = "Connection string or name",
    Description = "Azure Service Bus connection string or configured name.",
    Category = "Connection",
    Secret = true,
    Required = true,
    HasRequired = true,
    RestartRequired = true)]
public string ConnectionStringOrName { get; set; } = string.Empty;
```

The metadata fields used by the 3.8 generator include:

| Field                        | Use it for                                                     |
| ---------------------------- | -------------------------------------------------------------- |
| `DisplayName`, `Description` | Operator-facing labels and help text.                          |
| `Category`, `Group`          | Organizing related settings.                                   |
| `Required`, `HasRequired`    | Required-setting metadata.                                     |
| `DefaultValue`               | A deployment default.                                          |
| `Secret`, `Sensitive`        | Sensitivity metadata.                                          |
| `RestartRequired`            | The setting takes effect after a restart.                      |
| `Advanced`, `Experimental`   | Progressive disclosure and release status.                     |
| `UIHint`                     | A consumer-facing hint for choosing an editor or presentation. |

`Secret` and `Sensitive` are manifest hints, not secret storage. Keep connection strings, tokens, and passwords in environment variables, a secret manager, or another deployment-specific provider. The feature code must still bind the value to the configuration section it actually uses. If the value is intended to be a named Elsa workflow secret, see the [Secrets management guide](/guides/security/secrets-management) for the separate runtime feature and its Studio picker.

For a modular host, the manifest setting name is not by itself the full configuration path. The host places feature settings under the shell's `Settings` object, and the feature selects the section it binds. For example, the released MassTransit Azure Service Bus feature binds `MassTransitAzureServiceBus:ConnectionStringOrName`, which can be represented in the modular host configuration as:

```json
{
  "CShells": {
    "Shells": [
      {
        "Name": "Default",
        "Settings": {
          "MassTransitAzureServiceBus": {
            "ConnectionStringOrName": "${ConnectionStrings:Messaging}"
          }
        }
      }
    ]
  }
}
```

The package manifest documents the setting; the shell configuration and feature binding determine whether the value is actually used. See [Standalone and Modular Hosting](/guides/architecture/standalone-and-modular-hosting) and [Configuration Management](/guides/deployment/configuration-management) for host-specific configuration shapes.

For example, the released Quartz feature marks scheduler properties as restart-required because they affect service registration and lifecycle behavior. The manifest does not perform that restart; it communicates the operational consequence.

## Build, inspect, and pack

Build the package before packing it, then inspect the generated manifest:

```bash
dotnet build -c Release
find obj -name elsa-package.json -print
dotnet pack -c Release
unzip -p bin/Release/*.nupkg elsa-package.json
```

The generator's defaults generate the manifest and include it in the package. If a pipeline uses `dotnet pack --no-build`, the manifest must already exist under the matching `obj/<configuration>/<target-framework>/` directory. A missing manifest causes packing to fail when inclusion is enabled. Use the generator's MSBuild properties to change this behavior, including `GenerateElsaPackageManifest`, `ElsaPackageManifestOutputPath`, and `ElsaPackageManifestIncludeInPackage`.

When metadata cannot be inferred from code, add an `elsa-package.overrides.json` file beside the project file and pass it with `ElsaPackageManifestOverrideFile`. Treat override data as packaging metadata: it does not change the feature's runtime configuration.

## What this means for Studio users

An extension package can be compatible with Elsa Server, contribute activities that a Studio user can place on a canvas, or provide a Studio-specific feature. These are different concerns. The `elsa.server` hint in the released Core and Extensions packages describes the package's runtime compatibility; it does not mean that Studio will provision its database or message broker.

For a Studio-facing extension, document the separate Studio host registration, activity or UI registration, backend URL, and any required permissions. Verify the behavior in the target Studio host rather than assuming that a package manifest turns `ManifestSetting` metadata into a designer editor.

## Extension author checklist

* Add the generator as a private build dependency for the package projects that contain CShells features.
* Declare package- or feature-level runtime compatibility.
* Add infrastructure metadata where a feature depends on an external resource.
* Mark only deploy-time properties as manifest settings.
* Make `ConfigurationKeys` match the names your feature actually reads.
* Keep sensitive values out of source-controlled JSON and example defaults.
* Build, inspect `elsa-package.json`, and verify the `.nupkg` contains the file.
* Document the actual Server and Studio registration paths separately.

For the complete feature implementation behind the examples, see the [released Azure Service Bus shell feature](https://github.com/elsa-workflows/elsa-extensions/blob/d407e9621770a55427ac6c2315bd779da08d5fea/src/modules/servicebus/Elsa.ServiceBus.AzureServiceBus/ShellFeatures/AzureServiceBusShellFeature.cs) and [released Quartz shell feature](https://github.com/elsa-workflows/elsa-extensions/blob/d407e9621770a55427ac6c2315bd779da08d5fea/src/modules/scheduling/Elsa.Scheduling.Quartz/ShellFeatures/QuartzFeature.cs).


# Extensibility

Learn how to extend Elsa Workflows v3 with custom modules and plugins. Covers module registration, contributing activities, services, and API endpoints with practical examples.

Elsa Workflows v3 is built on a powerful **module and plugin architecture** that makes it easy to extend the framework with custom functionality. This guide explains what modules are, how they work, and how to create your own modules that contribute activities, services, and even API endpoints.

## What is a Module?

In Elsa v3, a **module** is a logical unit that groups related functionality together. Think of a module as a plugin that can be "installed" into your Elsa application to add new capabilities.

### Key Characteristics of Modules

* **Self-contained**: Each module encapsulates related features
* **Composable**: Modules can be mixed and matched
* **Configurable**: Modules expose configuration options via fluent API
* **Discoverable**: Modules follow a consistent naming and registration pattern

### Module vs Feature

Elsa's architecture uses two related concepts:

| Concept     | Purpose                                                            | Example                       |
| ----------- | ------------------------------------------------------------------ | ----------------------------- |
| **Module**  | Container for features, exposed via `IModule`                      | The Elsa configuration object |
| **Feature** | Self-contained unit of functionality inheriting from `FeatureBase` | `HttpFeature`, `EmailFeature` |

In practice, you'll typically create **features** and register them with the Elsa **module** using extension methods.

## How Modules are Registered

Modules are registered during application startup using the `AddElsa()` method:

```csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddElsa(elsa => elsa
    .UseWorkflowRuntime()      // Adds workflow runtime feature
    .UseHttp()                 // Adds HTTP activities and triggers
    .UseEmail()                // Adds email activities
    .UseJavaScript()           // Adds JavaScript expression support
    .UseMyCustomModule()       // Your custom module
);
```

Each `UseXyz()` method is an extension method that:

1. Creates or retrieves a feature instance
2. Configures the feature (optional)
3. Registers services and activities
4. Returns the module for method chaining

## Module Contributions

Modules can contribute three main types of functionality to Elsa:

### 1. Activities

Custom activities that workflow designers can use in their workflows.

### 2. Services

Services registered with dependency injection that activities and other components can consume.

### 3. API Endpoints

REST API endpoints that extend Elsa Server's capabilities.

Let's explore each of these with practical examples.

## Creating a Custom Module

We'll create a complete example module called `MyReportingModule` that demonstrates all three contribution types.

### Step 1: Create the Feature Class

A feature inherits from `FeatureBase` and defines what gets registered:

```csharp
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Microsoft.Extensions.DependencyInjection;

namespace MyCompany.Elsa.Reporting.Features;

/// <summary>
/// Provides reporting capabilities for workflows.
/// </summary>
public class ReportingFeature : FeatureBase
{
    public ReportingFeature(IModule module) : base(module)
    {
    }

    /// <summary>
    /// Configure services, activities, and options.
    /// </summary>
    public override void Configure()
    {
        // Register all activities from this assembly
        Module.AddActivitiesFrom<ReportingFeature>();
        
        // Register custom services
        Services.AddSingleton<IReportGenerator, ReportGenerator>();
        Services.AddScoped<IReportRepository, ReportRepository>();
        
        // Configure workflow options
        Module.ConfigureWorkflowOptions(options =>
        {
            // Add any workflow-level configuration here
        });
    }

    /// <summary>
    /// Post-configuration logic (optional).
    /// Called after all features have been configured.
    /// </summary>
    public override void Apply()
    {
        // Optional: Perform actions that depend on other features
        // being fully configured
    }
}
```

### Step 2: Create a Custom Activity

Create an activity that uses the registered service:

```csharp
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;

namespace MyCompany.Elsa.Reporting.Activities;

/// <summary>
/// Generates a report based on workflow data.
/// </summary>
[Activity(
    Namespace = "MyCompany.Reporting",
    Category = "Reporting",
    Description = "Generates a report and stores it")]
public class GenerateReport : CodeActivity<string>
{
    private readonly IReportGenerator _reportGenerator;

    public GenerateReport(IReportGenerator reportGenerator)
    {
        _reportGenerator = reportGenerator;
    }

    /// <summary>
    /// The report name.
    /// </summary>
    [Input(
        Description = "The name of the report to generate",
        UIHint = "single-line")]
    public Input<string> ReportName { get; set; } = default!;

    /// <summary>
    /// The report data as JSON.
    /// </summary>
    [Input(
        Description = "Data to include in the report as JSON",
        UIHint = "multi-line")]
    public Input<string?> Data { get; set; } = default!;

    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
    {
        var reportName = context.Get(ReportName);
        var data = context.Get(Data) ?? "{}";
        
        // Generate the report using our service
        var reportId = await _reportGenerator.GenerateAsync(reportName, data);
        
        // Output the report ID
        context.Set(Result, reportId);
        
        // Log to journal for debugging
        context.JournalData.Add("ReportId", reportId);
        context.JournalData.Add("ReportName", reportName);
    }
}
```

### Step 3: Create the Service Implementation

Implement the service that the activity depends on:

```csharp
namespace MyCompany.Elsa.Reporting.Services;

public interface IReportGenerator
{
    Task<string> GenerateAsync(string reportName, string data);
}

public class ReportGenerator : IReportGenerator
{
    private readonly IReportRepository _repository;
    private readonly ILogger<ReportGenerator> _logger;

    public ReportGenerator(
        IReportRepository repository,
        ILogger<ReportGenerator> logger)
    {
        _repository = repository;
        _logger = logger;
    }

    public async Task<string> GenerateAsync(string reportName, string data)
    {
        _logger.LogInformation("Generating report: {ReportName}", reportName);
        
        // Generate report ID
        var reportId = Guid.NewGuid().ToString();
        
        // Create report content (simplified example)
        var report = new Report
        {
            Id = reportId,
            Name = reportName,
            Data = data,
            GeneratedAt = DateTime.UtcNow
        };
        
        // Store the report
        await _repository.SaveAsync(report);
        
        _logger.LogInformation("Report generated: {ReportId}", reportId);
        
        return reportId;
    }
}

public interface IReportRepository
{
    Task SaveAsync(Report report);
    Task<Report?> GetByIdAsync(string id);
}

public class ReportRepository : IReportRepository
{
    // Simplified in-memory repository
    private readonly Dictionary<string, Report> _reports = new();

    public Task SaveAsync(Report report)
    {
        _reports[report.Id] = report;
        return Task.CompletedTask;
    }

    public Task<Report?> GetByIdAsync(string id)
    {
        _reports.TryGetValue(id, out var report);
        return Task.FromResult(report);
    }
}

public class Report
{
    public string Id { get; set; } = default!;
    public string Name { get; set; } = default!;
    public string Data { get; set; } = default!;
    public DateTime GeneratedAt { get; set; }
}
```

### Step 4: Add API Endpoints (Optional)

Expose an API endpoint for accessing generated reports:

```csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;

namespace MyCompany.Elsa.Reporting.Endpoints;

/// <summary>
/// Provides API endpoints for the reporting module.
/// </summary>
public static class ReportingEndpoints
{
    public static IEndpointRouteBuilder MapReportingEndpoints(
        this IEndpointRouteBuilder endpoints)
    {
        var group = endpoints.MapGroup("/reporting");
        
        // Health check endpoint for the reporting module
        group.MapGet("/health", () => Results.Ok(new 
        { 
            module = "reporting",
            status = "healthy",
            timestamp = DateTime.UtcNow
        }))
        .WithName("ReportingHealth")
        .WithTags("Reporting");
        
        // Get report by ID
        group.MapGet("/reports/{id}", async (
            string id,
            IReportRepository repository) =>
        {
            var report = await repository.GetByIdAsync(id);
            return report != null 
                ? Results.Ok(report) 
                : Results.NotFound();
        })
        .WithName("GetReport")
        .WithTags("Reporting");
        
        return endpoints;
    }
}
```

To register these endpoints, update your feature:

```csharp
public override void Apply()
{
    // Register endpoint configuration
    Services.Configure<WebApplicationOptions>(options =>
    {
        // Note: Actual endpoint mapping happens in Program.cs
        // This is just for documentation purposes
    });
}
```

Then in your `Program.cs`, after building the app:

```csharp
var app = builder.Build();

// Map Elsa API endpoints
app.UseWorkflowsApi();

// Map custom reporting endpoints
app.MapReportingEndpoints();

app.Run();
```

### Step 5: Create Extension Methods

Create a fluent extension method following Elsa conventions:

```csharp
using Elsa.Features.Services;
using MyCompany.Elsa.Reporting.Features;

namespace MyCompany.Elsa.Reporting.Extensions;

public static class ReportingModuleExtensions
{
    /// <summary>
    /// Adds reporting capabilities to Elsa.
    /// </summary>
    public static IModule UseReporting(
        this IModule module,
        Action<ReportingFeature>? configure = null)
    {
        module.Use(configure);
        return module;
    }
}
```

### Step 6: Use Your Module

Now you can use your custom module in any Elsa application:

```csharp
// Program.cs
using MyCompany.Elsa.Reporting.Extensions;
using MyCompany.Elsa.Reporting.Endpoints;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddElsa(elsa => elsa
    .UseWorkflowRuntime()
    .UseReporting()  // Your custom module!
);

var app = builder.Build();

app.UseWorkflowsApi();
app.MapReportingEndpoints();

app.Run();
```

The `GenerateReport` activity is now available in Elsa Studio and can be used in workflows.

## Module Configuration Options

For more complex modules, provide configuration options:

```csharp
public class ReportingOptions
{
    public string StoragePath { get; set; } = "./reports";
    public int MaxReportSizeMb { get; set; } = 50;
    public bool EnableCompression { get; set; } = true;
}

public class ReportingFeature : FeatureBase
{
    public ReportingOptions Options { get; set; } = new();

    public ReportingFeature(IModule module) : base(module)
    {
    }

    public override void Configure()
    {
        Module.AddActivitiesFrom<ReportingFeature>();
        
        // Register services with options
        Services.AddSingleton(Options);
        Services.AddSingleton<IReportGenerator, ReportGenerator>();
    }
}

// Extension method with configuration
public static IModule UseReporting(
    this IModule module,
    Action<ReportingOptions>? configure = null)
{
    return module.Use<ReportingFeature>(feature =>
    {
        if (configure != null)
        {
            configure(feature.Options);
        }
    });
}

// Usage with configuration
builder.Services.AddElsa(elsa => elsa
    .UseReporting(options =>
    {
        options.StoragePath = "/data/reports";
        options.MaxReportSizeMb = 100;
        options.EnableCompression = false;
    })
);
```

## Module Discovery Pattern

Modules in Elsa follow a consistent pattern that makes them easy to discover and use:

1. **Naming Convention**:
   * Feature: `XyzFeature`
   * Extension method: `UseXyz()`
   * Options: `XyzOptions`
2. **Registration Flow**:

   ```
   UseXyz() -> Creates/Configures Feature -> Feature.Configure() 
   -> Registers Services/Activities -> Feature.Apply()
   ```
3. **Method Chaining**:

   ```csharp
   .UseWorkflowRuntime()
   .UseHttp()
   .UseEmail()
   .UseReporting()  // All return IModule
   ```

## Complete Module Structure

Here's the recommended structure for a module project:

```
MyCompany.Elsa.Reporting/
├── Activities/
│   ├── GenerateReport.cs
│   └── ExportReport.cs
├── Features/
│   ├── ReportingFeature.cs
│   └── ReportingOptions.cs
├── Services/
│   ├── IReportGenerator.cs
│   ├── ReportGenerator.cs
│   ├── IReportRepository.cs
│   └── ReportRepository.cs
├── Endpoints/
│   └── ReportingEndpoints.cs
├── Extensions/
│   └── ReportingModuleExtensions.cs
└── Models/
    └── Report.cs
```

## Best Practices

### 1. Follow Naming Conventions

* Use `XyzFeature` for feature classes
* Use `UseXyz()` for extension methods
* Use `XyzOptions` for configuration classes

### 2. Minimal Dependencies

* Only reference necessary Elsa packages
* Keep third-party dependencies minimal
* Use interfaces for external dependencies

### 3. Configuration Over Convention

* Provide sensible defaults
* Allow configuration via options
* Document all configuration properties

### 4. Documentation

* Add XML documentation to all public APIs
* Include examples in feature descriptions
* Document activity inputs and outputs

### 5. Testing

* Unit test activities independently
* Integration test features
* Test with different configurations

## Packaging as NuGet

To share your module as a NuGet package:

```xml
<!-- MyCompany.Elsa.Reporting.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <PackageId>MyCompany.Elsa.Reporting</PackageId>
    <Version>1.0.0</Version>
    <Authors>Your Name</Authors>
    <Description>Reporting module for Elsa Workflows</Description>
    <PackageTags>elsa;workflows;reporting</PackageTags>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Elsa" Version="3.7.0" />
    <PackageReference Include="Elsa.Workflows.Core" Version="3.7.0" />
    <PackageReference Include="Elsa.Workflows.Runtime" Version="3.7.0" />
  </ItemGroup>
</Project>
```

Build and publish:

```bash
dotnet pack -c Release
dotnet nuget push bin/Release/MyCompany.Elsa.Reporting.1.0.0.nupkg
```

## Real-World Examples

Elsa's built-in features serve as excellent examples:

### HTTP Feature

```csharp
builder.Services.AddElsa(elsa => elsa
    .UseHttp(http => 
    {
        http.ConfigureHttpOptions(options =>
        {
            options.BaseUrl = new Uri("https://api.example.com");
        });
    })
);
```

### Email Feature

```csharp
builder.Services.AddElsa(elsa => elsa
    .UseEmail(email =>
    {
        email.ConfigureOptions(options =>
        {
            options.SmtpHost = "smtp.example.com";
            options.SmtpPort = 587;
        });
    })
);
```

### MassTransit Feature

```csharp
builder.Services.AddElsa(elsa => elsa
    .UseMassTransit(mt =>
    {
        mt.UseRabbitMq("amqp://localhost");
    })
);
```

## Further Reading

* [DropIns](/guides/modules-and-plugins/dropins) for independently deployed server-side extension assemblies and packages.
* [**Custom Activities**](/extensibility/custom-activities) - Detailed guide on creating activities
* [**Plugins & Modules**](/guides/plugins-modules) - Extended guide with more examples
* [**Architecture Overview**](/guides/architecture) - Understanding Elsa's architecture
* [**HTTP Workflows**](/guides/http-workflows) - Example of the HTTP module in action

## Summary

Creating custom modules in Elsa v3:

1. **Create a Feature** - Inherit from `FeatureBase`
2. **Register Components** - Add activities, services, and configuration
3. **Create Extension Method** - Follow the `UseXyz()` pattern
4. **Package & Share** - Distribute as NuGet for reuse

With this module architecture, you can extend Elsa to fit any domain or integration scenario while maintaining consistency with the rest of the Elsa ecosystem.


# DropIns

Package and discover server-side Elsa extensions from a DropIns directory in Elsa 3.8.0.

The DropIns module lets an Elsa Server load extension assemblies or NuGet packages from a directory at runtime. Use it when the host should discover trusted extension artifacts without adding each extension as a compile-time project reference.

DropIns is a server-side deployment mechanism. It does not install NuGet dependencies, expose a package feed, or add a client-side Elsa Studio plugin. If an extension also needs an activity catalog, variable type, API endpoint, or Studio editor, configure those surfaces separately on the server and in Studio.

## When to use DropIns

Choose the extension path that matches the deployment model:

| Need                             | Prefer                                                            |
| -------------------------------- | ----------------------------------------------------------------- |
| Fixed build-time extension       | Normal package reference and `Use...`/`Add...` registration       |
| Independent trusted artifact     | DropIns                                                           |
| Studio-only visual customization | [Studio customization](/guides/studio/customization)              |
| Activity catalog contribution    | [Activity type providers](/extensibility/activity-type-providers) |

DropIns are most useful for controlled server deployments. Every assembly or package under the configured directory is inspected and may execute code, so do not point the directory at an upload location or an untrusted shared path.

## Enable DropIns in the host

Reference `Elsa.DropIns` from the server host and call `InstallDropIns` while configuring Elsa. The release workbench uses an `App_Data/DropIns` directory; an application can choose another location.

```csharp
using Elsa.DropIns.Extensions;
using Elsa.Extensions;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddElsa(elsa => elsa
    .InstallDropIns(options =>
    {
        options.DropInRootDirectory = Path.Combine(
            builder.Environment.ContentRootPath,
            "App_Data",
            "DropIns");
    }));
```

`DropInRootDirectory` is a required operational setting. The monitor creates the directory if it does not exist, but the host must still provide a usable path. In a container, mount or copy the directory into every server replica that is expected to load the same extensions.

## Author a DropIn

Reference `Elsa.DropIns.Core` from the extension project. Implement `IDropIn` with a public parameterless constructor:

```csharp
using System;
using System.Threading;
using System.Threading.Tasks;
using Elsa.DropIns.Core;
using Elsa.Features.Services;
using Microsoft.Extensions.DependencyInjection;

public sealed class AcmeDropIn : IDropIn
{
    public void Install(IModule module)
    {
        // This runs while Elsa is building its module configuration.
        module.Services.AddSingleton<AcmeGreetingService>();
    }

    public ValueTask ConfigureAsync(
        IServiceProvider serviceProvider,
        CancellationToken cancellationToken)
    {
        // This runs after the host service provider is available.
        return ValueTask.CompletedTask;
    }

    public void Unconfigure(IServiceProvider serviceProvider)
    {
        // Release resources owned by this DropIn when its artifact is deleted.
    }
}

public sealed class AcmeGreetingService
{
}
```

The three methods have different responsibilities:

* `Install(IModule)` registers services or configures Elsa features. It runs during `InstallDropIns`, before the application service provider is built.
* `ConfigureAsync(...)` performs initialization that needs the application service provider. It is called for each discovered DropIn during the monitor's initial scan and when a watched path changes.
* `Unconfigure(...)` releases resources when the monitor processes a deleted artifact. It is not a general rollback mechanism for registrations made by `Install`.

The loader finds exported, non-abstract, non-interface implementations of `IDropIn` and creates them with `Activator.CreateInstance`. Keep the DropIn constructor parameterless and put dependency resolution in the lifecycle methods instead.

## Deploy an artifact

The directory catalog recursively scans the configured root for both `.dll` and `.nupkg` files.

### Deploy assemblies

Copy the DropIn assembly and its dependencies into an isolated subdirectory:

```
App_Data/
  DropIns/
    Acme/
      Acme.Elsa.DropIn.dll
      Acme.Dependency.dll
```

The directory loader uses an assembly dependency resolver for the assembly path. Keep the DropIn's dependency set together and avoid copying the whole host's publish directory into the DropIns directory: every DLL below the root is inspected for `IDropIn` implementations.

### Deploy NuGet packages

Copy a `.nupkg` into the root or a subdirectory:

```
App_Data/
  DropIns/
    Acme/
      Acme.Elsa.DropIn.<version>.nupkg
```

The release loader reads every DLL in the package archive and searches the loaded assemblies for `IDropIn` implementations. This is not package restore: the package must already contain the assemblies the DropIn needs, and the artifact must be trusted before it is copied into the directory.

Use an `Elsa.DropIns.Core` package version compatible with the server's Elsa release. The DropIn is loaded into a separate assembly load context, but its contract types still need to resolve to the host-compatible Elsa assemblies.

## Understand the lifecycle

`InstallDropIns` performs two related actions:

1. It scans the root immediately and calls `Install` on each discovered DropIn, allowing the DropIn to contribute to the `IModule`.
2. It registers `DropInDirectoryMonitorHostedService`, which creates the root directory if necessary, performs an initial scan, and calls `ConfigureAsync` with the real application service provider.

The monitor uses a `FileSystemWatcher` recursively. In the 3.8.0 source it handles `Changed` and `Deleted` events, debounced by two seconds. When the monitor has recorded instances for a deleted event path, those instances receive `Unconfigure`. The initial scan is keyed differently in the release implementation, so deletion is not a reliable rollback for every initially loaded artifact. A changed artifact is scanned and configured again; the module does not provide a transactional replacement operation.

Treat replacement as a deployment operation with a health check and, when necessary, a restart. Do not assume that changing a file atomically adds a new DropIn, rolls back service registrations, or unloads all assembly memory. The release source calls `Unconfigure` for deletion but does not expose a public assembly-unload or rollback contract.

## Troubleshoot loading

Check these boundaries in order:

1. Confirm the server calls `InstallDropIns` and that `DropInRootDirectory` resolves to the directory you populated.
2. Confirm the artifact extension is `.dll` or `.nupkg` and that it is below the root, including any required dependencies.
3. Confirm the DropIn type is public, concrete, implements `IDropIn`, and has a public parameterless constructor.
4. Check startup and hosted-service logs. `Install` runs during module setup; `ConfigureAsync` and monitor errors occur at a different lifecycle stage.
5. If a changed artifact behaves unpredictably, remove it only after the extension has released external resources, then restart the host to obtain a clean module and assembly state.

DropIns do not automatically update Studio. After a server extension is loaded, Studio can use only the capabilities the server exposes through its normal APIs and the Studio packages/configuration you have installed. For reusable server modules, start with [Modules and Plugins](/guides/modules-and-plugins), then use the focused guides for [custom activities](/extensibility/custom-activities), [activity type providers](/extensibility/activity-type-providers), and [custom types](/extensibility/custom-types).

## Release source

The behavior described here is implemented in the [release/3.8.0 DropIns source directory](https://github.com/elsa-workflows/elsa-extensions/tree/release/3.8.0/src/modules/dropins). Key files include `Elsa.DropIns.Core/IDropIn.cs`, `Elsa.DropIns/Extensions/ModuleExtensions.cs`, `Elsa.DropIns/Catalogs/DirectoryDropInCatalog.cs`, and `Elsa.DropIns/HostedServices/DropInDirectoryMonitorHostedService.cs`.


# Testing & Debugging Workflows

Test custom Elsa activities and workflows with the released Elsa testing fixtures, then diagnose real workflow instances with the journal and runtime tools.

Test the smallest useful unit first, then test the workflow's routing and runtime configuration. This guide uses the testing helpers in Elsa Core `release/3.8.0`; their source is the best reference when your test needs a feature that is not shown here.

| What you need confidence in                                       | Start with                               | What to assert                                        |
| ----------------------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------- |
| A custom activity's inputs, outputs, or service call              | `ActivityTestFixture`                    | The service interaction, output, or execution context |
| Branching, sequencing, outcomes, and registered workflow behavior | `WorkflowTestFixture`                    | Workflow status and the activity journal              |
| HTTP, persistence, authentication, or a message broker            | Your application's integration test host | The public boundary and persisted result              |
| A workflow already running outside a test                         | Elsa Studio and the runtime APIs         | Instance state, journal, incidents, and logs          |

## Add the testing helpers

Create an xUnit test project and keep the fixture packages on the same Elsa version as the packages used by the workflow application. Do not mix fixture and runtime versions.

```bash
dotnet add package Elsa.Testing.Shared --version x.y.z
dotnet add package Elsa.Testing.Shared.Integration --version x.y.z
dotnet add package NSubstitute
```

`Elsa.Testing.Shared` supplies the focused activity fixture. `Elsa.Testing.Shared.Integration` supplies the workflow fixture and its journal-oriented assertion helpers. Both expose their types through the `Elsa.Testing.Shared` namespace. If the matching version is prerelease, add `--prerelease` or specify that prerelease version explicitly.

## Unit-test a custom activity

Use `ActivityTestFixture` when the behavior belongs to one activity. It builds a minimal workflow execution context, registers the activity type, evaluates its input properties, and executes the activity. It already includes the core workflow services; register only the dependencies specific to the activity.

The following test follows the released Elsa Core test pattern for `WriteLine`:

{% code title="WriteLineTests.cs" %}

```csharp
using Elsa.Testing.Shared;
using Elsa.Workflows;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Xunit;

public class WriteLineTests
{
    [Fact]
    public async Task Writes_the_configured_text()
    {
        var writer = Substitute.For<TextWriter>();
        var streamProvider = Substitute.For<IStandardOutStreamProvider>();
        streamProvider.GetTextWriter().Returns(writer);

        var activity = new WriteLine("Order accepted");

        await new ActivityTestFixture(activity)
            .ConfigureServices(services => services.AddSingleton(streamProvider))
            .ExecuteAsync();

        writer.Received(1).WriteLine("Order accepted");
    }
}
```

{% endcode %}

For your own activity, replace the standard-output provider with the narrow interface the activity depends on and assert its call. This keeps tests deterministic and avoids starting a server or database just to test business logic.

### Configure state deliberately

`ConfigureServices(...)` adds fakes, options, or application services before the fixture builds its service provider. `ConfigureContext(...)` receives the `ActivityExecutionContext` immediately before the activity runs. Use the latter only when the behavior depends on workflow state, variables, or correlation that cannot be expressed through normal activity inputs.

```csharp
var context = await new ActivityTestFixture(activity)
    .ConfigureContext(
        context => context.WorkflowExecutionContext.CorrelationId = "order-42")
    .ExecuteAsync();
```

Assert an observable result: a call to a substituted dependency, an output on `context`, or a state change. Avoid asserting the fixture's implementation details.

## Test routing with a workflow fixture

`WorkflowTestFixture` is the next level up. Its baseline configuration adds core activities, scheduling, C#, JavaScript, Liquid, workflow management, and an xUnit-backed output stream. It builds and activates the test services on first use.

For an in-memory workflow, run an activity or workflow and inspect the returned journal. The helper methods below are provided by the released fixture package.

{% code title="RoutingTests.cs" %}

```csharp
using Elsa.Testing.Shared;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Xunit;
using Xunit.Abstractions;

public class RoutingTests(ITestOutputHelper output)
{
    private readonly WorkflowTestFixture _fixture = new(output);

    [Fact]
    public async Task Runs_the_selected_branch()
    {
        var selected = new WriteLine("selected");
        var skipped = new WriteLine("skipped");
        var flow = new If
        {
            Condition = new(() => true),
            Then = selected,
            Else = skipped
        };

        var result = await _fixture.RunActivityAsync(flow);

        result.AssertWorkflowCompleted();
        result.AssertActivitiesCompleted(flow, selected);
        result.AssertActivityNotExecuted(skipped);
        Assert.Contains("selected", _fixture.CapturingTextWriter.Lines);
    }
}
```

{% endcode %}

The result's journal records the activity execution contexts. Use `AssertActivityCompleted`, `AssertActivityNotExecuted`, and `AssertActivityExecutionCount` to assert control flow rather than relying only on output text. `GetActivityStatus(...)` and `GetOutcomes(...)` on the fixture are useful when a test needs the raw status or named outcomes.

### Test registered definitions and custom activity assemblies

When a test must exercise registration rather than an in-memory activity, configure the fixture before its first run:

```csharp
private readonly WorkflowTestFixture _fixture = new(output)
    .AddActivitiesFrom<MyCustomActivity>()
    .AddWorkflow<OrderWorkflow>();

var result = await _fixture.RunWorkflowAsync<OrderWorkflow>();
result.AssertWorkflowCompleted();
```

`ConfigureElsa(...)` adds Elsa features needed by the behavior under test, and `ConfigureServices(...)` adds application services. The fixture can also load workflow definitions from a relative directory with `WithWorkflowsFromDirectory(...)`. Use those options when testing a workflow definition or activity package as it is registered, not merely its code path.

## Test the boundary that can fail in production

The fixtures do not replace integration tests for a host. Add a small number of tests through your application's actual boundary when a workflow depends on:

* HTTP routing, authentication, or request/response behavior
* persistence, transactions, or a distributed cache
* timers, queues, broker consumers, or background workers
* external service contracts

Keep these tests scenario-focused: start or resume the workflow through the same boundary production uses, then assert the durable workflow state and the external effect. For workflows that wait, test both sides: bookmark creation and the stimulus that resumes it. See [Long-running Workflows](/guides/running-workflows/long-running-workflows) for the waiting and resumption model.

## Diagnose a workflow that is not a test failure

Automated tests explain expected behavior; the execution journal explains what happened to a particular instance. For an issue found in Studio or production:

1. Reproduce with representative, non-sensitive inputs if possible.
2. Find the instance and inspect its status, journal entries, activity records, incidents, and variables using [Investigate a Workflow Instance](/operate/workflow-state-and-journal).
3. Compare the executed activity path with the workflow test that covers the same rule; add a regression test before changing the workflow.
4. Use [Troubleshooting](/guides/troubleshooting) for host logs, database, scheduler, and clustered-runtime checks. Add [Distributed Tracing](/operate/distributed-tracing) when a request crosses services.

For interactive designer checks, the Studio tour explains the execution journal and supported activity testing in [Studio Tour & Troubleshooting](/studio/studio-tour-troubleshooting).

## Release-backed references

* [ActivityTestFixture source](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/src/common/Elsa.Testing.Shared/ActivityTestFixture.cs)
* [WorkflowTestFixture source](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/src/common/Elsa.Testing.Shared.Integration/WorkflowTestFixture.cs)
* [Journal assertion helpers](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/src/common/Elsa.Testing.Shared.Integration/RunWorkflowResultAssertions.cs)
* [Released activity-fixture test example](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/test/unit/Elsa.Activities.UnitTests/Console/WriteLineTests.cs)


# Weaver and AI Workflow Assistance

Configure and use Elsa's release-backed AI Host and Weaver Studio workspace for grounded, reviewable workflow assistance.

Weaver is Elsa's AI-assisted workspace for asking questions about workflow definitions and instances, inspecting grounded runtime context, and producing reviewable workflow proposals. It is a server-mediated feature: Studio sends references and messages to Elsa Server, the AI Host resolves authorized context and tools, and a configured provider performs the model interaction.

This guide describes the implementation shipped in `release/3.8.0`. Treat it as a capability and deployment guide, not as a promise that every planned AI API is already available.

## Where the pieces fit

* **Weaver (Studio)** — chat UI, context attachments, streamed assistant and tool activity, and proposal notifications.
* **AI Host (Server)** — identity, tenant resolution, grounding, tool filtering, redaction, audit, and the stream contract.
* **AI provider (Server)** — provider sessions and provider-to-Elsa event mapping.
* **AI persistence (Server)** — durable conversations, proposals, and audit records when a persistence implementation is installed.

Studio does not host an AI runtime, invoke a model provider, or receive provider credentials. If the server does not advertise the AI shell feature, the Weaver menu is hidden and the page reports that Weaver is unavailable.

## Before enabling it

Use Weaver when a person needs help understanding or shaping an Elsa workflow with server-side context. It is a good fit for:

* a process designer asking about an attached workflow definition;
* a technical user investigating an attached workflow instance or incident;
* a domain specialist turning a described business process into a draft for technical review; and
* an engineering team that wants an auditable proposal workflow rather than direct, model-controlled changes.

Keep ordinary workflow APIs and Studio actions as the system of record. The AI Host's proposal tools do not write workflow definitions, and the current Studio module does not apply proposals.

## Install and compose the server

Add the Elsa AI Host module and one provider implementation to the server. The release includes the Copilot provider in Core. Add a provider-specific AI persistence package as well when conversations, proposals, and audit records must survive a restart. The persistence packages follow the normal Elsa database provider choices, including SQLite, SQL Server, PostgreSQL, MySQL, and Oracle.

For a CShells-based host, the release sample composes these modules with feature sections like the following. Keep tokens and other credentials in a secret provider; do not commit them to `appsettings.json`.

```json
{
  "CShells": {
    "Shells": {
      "Default": {
        "Features": {
          "AI": {
            "StreamingEnabled": true,
            "ConversationPersistenceEnabled": true,
            "ProposalReviewEnabled": true,
            "DefaultProviderName": "copilot",
            "Providers": [
              {
                "Name": "copilot",
                "Provider": "copilot",
                "Model": "<model>",
                "Enabled": true
              }
            ],
            "Agents": [
              {
                "Name": "workflow-author",
                "DisplayName": "Workflow author",
                "Description": "Creates safe workflow proposals"
              }
            ]
          },
          "CopilotAI": {
            "ProviderName": "copilot",
            "Model": "<model>"
          },
          "AIPersistence": {
            "ConnectionString": "<configured-by-secret-provider>"
          }
        }
      }
    }
  }
}
```

The exact configuration path is host-specific, but the option names above are the `AIHostOptions`, `CopilotOptions`, and AI persistence feature properties used by the release sample. `Grounding` limits can be configured when calling `AddAIHostServices` directly; the CShells shell feature currently exposes the host's top-level AI options but not a nested `Grounding` property.

For a custom host that already registers the AI shell feature and its HTTP endpoints, the provider boundary is provider-neutral:

```csharp
services.AddAIHostServices(options =>
{
    options.DefaultProviderName = "copilot";
    options.Providers =
    [
        new AIProviderOptions
        {
            Name = "copilot",
            Provider = "copilot",
            Model = "<model>",
            Enabled = true
        }
    ];
});

services.AddCopilotAIProvider(options =>
{
    options.Model = "<model>";
    // Configure RuntimePath or RuntimeUrl and authentication through your
    // deployment's secret/configuration provider.
});
```

If more than one enabled provider is available, set `DefaultProviderName`. The capabilities endpoint reports streaming as unavailable when it cannot select one provider deterministically.

### Choose durable storage for production

`AddAIHostServices` supplies an in-memory conversation store by default. That is suitable for development and tests, but it is process-local. Install and configure a provider-specific AI persistence module for a production server so that the required proposal and audit stores are also available. A durable conversation store is required for Studio reconnect after a server restart; the default reconnect grace window is five minutes.

The default retention and size limits are:

| Option                            |   Default |
| --------------------------------- | --------: |
| Conversation retention            |   30 days |
| Reconnect grace                   | 5 minutes |
| Maximum tool result               |    64 KiB |
| Maximum resolved context          |   128 KiB |
| Maximum items per grounding query |        25 |
| Maximum grounding result          |    64 KiB |

Tune these limits for the provider context window and the sensitivity of the data you allow the AI Host to inspect.

## Add Weaver to Studio

Register the Studio module with the same authenticated backend configuration used by the other remote modules:

```csharp
builder.Services.AddRemoteBackend(backendApiConfig);
builder.Services.AddWeaverModule(backendApiConfig);
```

The release Server and WASM hosts both use `AddWeaverModule`. The module uses the backend authentication handler, calls the metadata endpoints, and streams `POST /ai/chat` as server-sent events. It does not need an OpenAI, Copilot, or other provider credential in the browser.

Weaver appears only when the backend advertises `Elsa.AI.Host.ShellFeatures.AIFeature`. If the menu is missing, check the server feature composition and the authenticated backend URL before changing Studio code.

## Use Weaver in Studio

1. Open **Weaver** from the Studio menu.
2. Attach a workflow definition or workflow instance by its reference ID.
3. Select an advertised agent when the server exposes more than the default.
4. Ask a focused question, such as what caused an attached instance to fail or which activities are available for a particular process step.
5. Review the assistant response, tool activity, warnings, and any proposal notification before taking a normal Studio or API action.

The server resolves attachments. Send a reference and scope, not a copied workflow or runtime database dump. Supported attachment kinds are advertised by `GET /ai/capabilities`; the default kinds are workflow definitions, workflow instances, activities, diagnostics scopes, and time ranges.

## What the AI Host can do

The built-in tools are grouped by purpose:

* **Activities**: search installed activity descriptors and retrieve a descriptor.
* **Workflow definitions**: search definitions, retrieve a definition or graph, and find usages.
* **Draft validation and proposals**: validate a draft, propose a new workflow, or propose an update against a baseline version.
* **Runtime inspection**: search instances, inspect an instance, read execution history or activity state, and search or inspect incidents.

Read-only tools are enabled by default. The create and update proposal tools are disabled until the host explicitly enables them through the AI feature's governed tool-enablement path. Draft validation is read-only and does not persist the draft.

If a required server source is not registered—for example, the activity registry, workflow definition store, workflow instance store, or proposal store—the tool returns an unavailable result and the capabilities response reports the reason. This is useful when a modular host intentionally omits a feature, but it is not a model/provider failure.

## Proposal lifecycle and current release boundary

`workflows.proposeCreate` and `workflows.proposeUpdate` write an `AIProposal` record containing the draft payload, rationale, validation diagnostics, warnings, graph diff, actor, tenant, and (for updates) the baseline workflow version. They produce either a validated or blocked proposal; they do not persist a workflow definition.

The current `release/3.8.0` implementation exposes these server endpoints:

* `GET /ai/capabilities` — advertises AI capabilities and attachment kinds to Studio; requires `ai:capabilities:view`.
* `GET /ai/tools?agent=...` — lists tools visible to the current actor, tenant, and agent; requires `ai:tools:view`.
* `POST /ai/chat` — starts or reconnects a streamed chat turn; requires `ai:chat`.

The release does not yet expose proposal detail, approve, reject, or apply endpoints. Studio can display proposal events, but its Approve, Reject, and Apply buttons remain disabled. Do not document or build an integration around `/ai/proposals/{id}/approve` or `/ai/proposals/{id}/apply` until those endpoints land in a released backend.

The code also defines proposal permission names (`ai:proposals:view`, `ai:proposals:approve`, and `ai:proposals:apply`) for the governed action surface. Their presence does not mean that the corresponding action routes exist in this release.

## Security and governance

The AI Host derives the actor from the authenticated user and the tenant from the active tenant accessor or supported tenant claims. It ignores a client-supplied provider name and accepts a requested agent only when that agent is configured and its required permissions are present. Tool discovery and execution are filtered for the current actor and tenant.

Grounded results are bounded and redacted before they are returned to the model or Studio. Chat, tool, and provider activity goes through the AI Host's audit path. Even with these controls, treat model output as untrusted:

* keep AI endpoints behind the same authentication and authorization boundary as the rest of Elsa Server;
* grant only the permissions needed for the user and agent;
* leave proposal tools disabled until a human review process exists;
* use tenant-aware persistence and verify database isolation; and
* avoid attaching broad runtime scopes or sensitive data when a narrower workflow, instance, activity, or time-range reference is enough.

### Do not confuse Weaver with the Agents extension

The `Elsa.Agents.*` packages in `elsa-extensions` provide agent activities and workflow-oriented agent composition. Their `UseAgents` and OpenAI/Azure OpenAI registration APIs are separate from Weaver's provider-neutral `IAIProvider` boundary. Installing an Agents package does not configure the Weaver menu or the `/ai/*` endpoints.

## Troubleshooting

### Weaver is missing from Studio

Confirm that the server composes `Elsa.AI.Host.ShellFeatures.AIFeature`, that the Studio backend URL is correct, and that the authenticated caller can read `GET /ai/capabilities`.

### Capabilities show no streaming

Check that at least one enabled provider is registered. If several providers are enabled, set `AIHostOptions.DefaultProviderName` to a provider name that the host can resolve.

### A tool is unavailable

Read the grounding capability's disabled reason. It usually means the related workflow, runtime, activity, or proposal store is not registered, or that the grounding family was disabled.

### A proposal never appears

Confirm that proposal tools were explicitly enabled and that an AI proposal store is registered. A blocked proposal can still be recorded; inspect its validation diagnostics rather than treating it as a provider error.

### Studio cannot reconnect

Conversation persistence must be enabled and backed by a durable conversation store. Reconnect is accepted only for an authorized conversation during the configured grace window.

## Release-source validation

This page was checked against the following `release/3.8.0` snapshots:

* Core [`5429008d`](https://github.com/elsa-workflows/elsa-core/tree/5429008d98a56afd29b4fd11107f7760710b1a64):
  * [AI Host README](https://github.com/elsa-workflows/elsa-core/blob/5429008d98a56afd29b4fd11107f7760710b1a64/src/modules/Elsa.AI.Host/README.md)
  * [Host options](https://github.com/elsa-workflows/elsa-core/blob/5429008d98a56afd29b4fd11107f7760710b1a64/src/modules/Elsa.AI.Host/Options/AIHostOptions.cs)
  * [AI endpoints](https://github.com/elsa-workflows/elsa-core/tree/5429008d98a56afd29b4fd11107f7760710b1a64/src/modules/Elsa.AI.Host/Endpoints/AI)
  * [Copilot provider](https://github.com/elsa-workflows/elsa-core/tree/5429008d98a56afd29b4fd11107f7760710b1a64/src/modules/Elsa.AI.Copilot)
* Studio [`d25f0aae`](https://github.com/elsa-workflows/elsa-studio/tree/d25f0aaeb5f14af6c5938d173aae828d87ebad5c):
  * [Weaver module](https://github.com/elsa-workflows/elsa-studio/tree/d25f0aaeb5f14af6c5938d173aae828d87ebad5c/src/modules/Elsa.Studio.AI)
  * [Weaver page](https://github.com/elsa-workflows/elsa-studio/blob/d25f0aaeb5f14af6c5938d173aae828d87ebad5c/src/modules/Elsa.Studio.AI/UI/Pages/Weaver.razor)
* Extensions [`335a2649`](https://github.com/elsa-workflows/elsa-extensions/tree/335a26495318f6ee1528bf2723b7333c753ce9a2):
  * [Agents module](https://github.com/elsa-workflows/elsa-extensions/tree/335a26495318f6ee1528bf2723b7333c753ce9a2/src/modules/agents)

The latest released branch remained `release/3.8.0` in all three source repositories; the refs had advanced since the previous inventory, so this page uses the commits above.


# Running Workflows

There are multiple ways to run a workflow:

* Using Elsa Studio.
* Using a trigger, such as HTTP Endpoint.
* Using timer and scheduled triggers.
* Using long-running workflow patterns such as bookmarks, callbacks, and background task resumption.
* Using Dispatch Workflow Activity
* Using Bulk Dispatch Workflows Activity
* Using the Elsa REST API.
* Using the Elsa library.

In this guide, we will see an example of each of these methods.

If you need to start one child workflow from another workflow, see [Dispatch Workflow Activity](/guides/running-workflows/dispatch-workflow-activity). If you need to start one child workflow per item in a collection, see [Bulk Dispatch Workflows Activity](/guides/running-workflows/bulk-dispatch-workflows). If you need time-based starts or waits, see [Timer and Scheduled Workflows](/guides/running-workflows/timer-and-scheduled-workflows). If you need the release-backed runtime mental model behind these options, see [Execution Model](/guides/architecture/execution-model). If you need to understand how Studio drafts become published versions and which definition version an API call selects, see [Workflow Definition Version Lifecycle](/guides/running-workflows/workflow-definition-lifecycle). If you need to correct or steer an existing instance, see [Alter a Running Workflow Instance](/guides/running-workflows/altering-workflow-instances).

## Before you start <a href="#before-you-start" id="before-you-start"></a>

For this guide, you will need the following:

* An [Elsa Server](/application-types/elsa-server) project
* An [Elsa Studio](/getting-started/containers/docker#elsa-studio) instance

## Running Workflows via REST API

The Elsa Server exposes REST API endpoints that allow you to execute workflows programmatically. This is useful for integrating workflows into external applications or services.

### Execute a Workflow by Definition ID

To execute a workflow by its definition ID, send a POST request to the following endpoint:

```
POST /elsa/api/workflow-definitions/{definitionId}/execute
```

#### Example using cURL

```bash
curl --location --request POST 'https://localhost:5001/elsa/api/workflow-definitions/my-workflow/execute' \
--header 'Authorization: ApiKey YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
  "input": {
    "message": "Hello from API",
    "userId": 123
  },
  "correlationId": "optional-correlation-id"
}'
```

#### Example using HTTPie

```bash
http POST https://localhost:5001/elsa/api/workflow-definitions/my-workflow/execute \
  Authorization:"ApiKey YOUR_API_KEY" \
  input:='{"message":"Hello from API","userId":123}' \
  correlationId="optional-correlation-id"
```

#### Request Body Parameters

* `input` (optional): A dictionary of input values to pass to the workflow
* `correlationId` (optional): A correlation ID to associate with the workflow instance
* `name` (optional): A custom name for the workflow instance
* `triggerActivityId` (optional): The ID of a specific trigger activity to start from
* `versionOptions` (optional): Options for selecting the workflow version

#### Sample Response

```json
{
  "workflowState": {
    "id": "workflow-instance-id",
    "definitionId": "my-workflow",
    "definitionVersionId": "version-id",
    "status": "Finished",
    "subStatus": "Finished",
    "output": {
      "result": "Workflow completed successfully"
    }
  }
}
```

### Synchronous vs Asynchronous Execution

The Elsa Server REST API supports two execution modes:

**Synchronous Execution** (`/execute` endpoint):

* The HTTP request waits for the workflow to complete before returning a response
* Use this when the workflow is designed to return a result immediately (e.g., HTTP workflows with response activities)
* The response includes the final workflow state and any output values
* Timeout considerations: Long-running workflows may exceed HTTP timeout limits

**Asynchronous Execution** (`/dispatch` endpoint):

* The HTTP request returns immediately after queuing the workflow for execution
* Use this for long-running workflows or fire-and-forget scenarios
* The response includes the workflow instance ID for later status queries
* Recommended for workflows that don't need to respond synchronously

#### Example: Synchronous Execution

```bash
curl --location --request POST 'https://localhost:5001/elsa/api/workflow-definitions/my-workflow/execute' \
  --header 'Authorization: ApiKey YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data-raw '{"input": {"orderId": "12345"}}'
```

#### Example: Asynchronous Execution (Fire-and-Forget)

```bash
curl --location --request POST 'https://localhost:5001/elsa/api/workflow-definitions/my-workflow/dispatch' \
  --header 'Authorization: ApiKey YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data-raw '{"input": {"orderId": "12345"}}'
```

### Authentication

The REST API requires authentication. The `Authorization` header value depends on your authentication configuration:

**Common Authentication Schemes:**

1. **API Key Authentication** (most common in Elsa Server):

   ```
   Authorization: ApiKey YOUR_API_KEY
   ```
2. **Bearer Token Authentication** (JWT):

   ```
   Authorization: Bearer YOUR_JWT_TOKEN
   ```
3. **Basic Authentication**:

   ```
   Authorization: Basic BASE64_ENCODED_CREDENTIALS
   ```

#### Obtaining API Keys

The method for obtaining API keys depends on your Elsa Server setup:

* **Elsa Server with Identity**: Use the identity endpoints to register users and generate API keys
* **Custom Authentication**: Refer to your organization's authentication provider
* **Development/Testing**: API keys may be pre-configured in `appsettings.json` or generated via Elsa Studio

For detailed information about configuring authentication, setting up API keys, and implementing custom authentication schemes, see [Authentication & Authorization](/guides/authentication).

> **Important**: The `Authorization` header examples in this guide use `ApiKey YOUR_API_KEY` as a placeholder. Replace this with your actual authentication scheme and credentials based on your Elsa Server configuration. The authorization format and credentials depend on how authentication is configured in your Elsa Server instance.

### Troubleshooting REST API Execution

This section covers common issues when executing workflows via the REST API.

#### Issue: Workflow Starts but HTTP Response Activity Not Reached

**Symptoms:**

* You call the `/execute` endpoint to start an HTTP workflow
* The API returns immediately with a workflow instance ID
* The workflow starts executing but never reaches the HTTP Response activity
* The HTTP client receives an incomplete or unexpected response

**Possible Causes:**

1. **Wrong Endpoint**: Using `/dispatch` instead of `/execute`
   * **Solution**: Use the `/execute` endpoint for synchronous HTTP workflows that need to return a response. The `/dispatch` endpoint is fire-and-forget and returns immediately without waiting for workflow completion.
2. **Workflow Not Designed for Synchronous Execution**:
   * The workflow may contain blocking activities (delays, waiting for external events) that suspend execution
   * **Solution**: Ensure the workflow completes synchronously without suspension. Remove or reconfigure blocking activities for synchronous HTTP workflows.
3. **HTTP Response Activity Misconfigured**:
   * The HTTP Response activity may not be connected properly in the workflow graph
   * Output expressions may be incorrect or throw exceptions
   * **Solution**: Verify the workflow design in Elsa Studio. Check that the HTTP Response activity is on the execution path and its properties are correctly configured.
4. **Workflow Faults Before Reaching Response Activity**:
   * An activity before the HTTP Response activity throws an exception
   * **Solution**: Check the workflow execution logs and incidents for errors. Use the [Troubleshooting Guide](/guides/troubleshooting) to diagnose faulted activities.
5. **Timeout Issues**:
   * The workflow takes too long and the HTTP client times out
   * **Solution**: Increase the HTTP client timeout, or redesign the workflow to complete faster. For long-running workflows, use the `/dispatch` endpoint and implement a callback or polling mechanism.
6. **Missing HTTP Workflow Configuration**:

   * Elsa Server may not be configured to handle HTTP workflows properly
   * **Solution**: Ensure `UseHttp()` is called in the Elsa configuration and that the HTTP middleware is registered:

   ```csharp
   builder.Services.AddElsa(elsa =>
   {
       elsa.UseHttp(); // Required for HTTP workflows
   });

   // In the app builder
   app.UseWorkflowsApi();
   app.UseWorkflows(); // Registers HTTP endpoints for workflow triggers
   ```

#### Debugging Steps

1. **Check Workflow Execution Status**:

   ```bash
   curl --location 'https://localhost:5001/elsa/api/workflow-instances/{instanceId}' \
     --header 'Authorization: ApiKey YOUR_API_KEY'
   ```
2. **Review Execution Logs**:
   * Check the `WorkflowExecutionLog` table or use Elsa Studio to view the workflow execution history
   * Look for activities that faulted or didn't execute
3. **Verify Workflow Design**:
   * Open the workflow in Elsa Studio
   * Ensure the HTTP Response activity is reachable from the HTTP Endpoint trigger
   * Test the workflow in the Studio designer
4. **Enable Detailed Logging**:

   ```json
   {
     "Logging": {
       "LogLevel": {
         "Elsa": "Debug",
         "Elsa.Http": "Debug"
       }
     }
   }
   ```
5. **Test with a Simple Workflow**:
   * Create a minimal workflow: HTTP Endpoint → HTTP Response
   * If this works, incrementally add activities to identify the problematic step

For more troubleshooting guidance, see the [Troubleshooting Guide](/guides/troubleshooting).

## Running Workflows via the Library

You can also run workflows programmatically from your .NET application using Elsa's API client or by directly using the workflow runtime services.

### Using IWorkflowRunner

The `IWorkflowRunner` service executes workflows directly in-process. This is useful for short-lived workflows that don't require background execution.

```csharp
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Microsoft.Extensions.DependencyInjection;

// Setup service container
var services = new ServiceCollection();
services.AddElsa();
var serviceProvider = services.BuildServiceProvider();

// Define a workflow
var workflow = new Sequence
{
    Activities =
    {
        new WriteLine("Starting workflow..."),
        new WriteLine("Processing data..."),
        new WriteLine("Workflow completed!")
    }
};

// Get the workflow runner (IWorkflowRunner is in Elsa.Workflows namespace)
var workflowRunner = serviceProvider.GetRequiredService<IWorkflowRunner>();

// Execute the workflow
var result = await workflowRunner.RunAsync(workflow);

Console.WriteLine($"Workflow status: {result.WorkflowState.Status}");
```

### Using IWorkflowRuntime (New Client API)

For running workflows by definition ID with input parameters, use the new `IWorkflowRuntime` client API:

```csharp
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Messages;
using Elsa.Workflows.Models;
using Microsoft.Extensions.DependencyInjection;

// Assume you have a configured service provider with Elsa services
var workflowRuntime = serviceProvider.GetRequiredService<IWorkflowRuntime>();

// Create a workflow client
var client = await workflowRuntime.CreateClientAsync();

// Create and run a workflow instance with input
var result = await client.CreateAndRunInstanceAsync(new CreateAndRunWorkflowInstanceRequest
{
    WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId("my-workflow"),
    Input = new Dictionary<string, object>
    {
        ["message"] = "Hello from the library!",
        ["userId"] = 123
    },
    CorrelationId = "optional-correlation-id",
    IncludeWorkflowOutput = true
});

// Access the workflow state
var workflowState = result.WorkflowState;
Console.WriteLine($"Workflow status: {workflowState.Status}");

// Access output if available
if (workflowState.Output != null)
{
    foreach (var output in workflowState.Output)
    {
        Console.WriteLine($"Output {output.Key}: {output.Value}");
    }
}
```

### Using IWorkflowRuntime (Legacy API - Obsolete)

> **Note:** The following API is marked as obsolete in Elsa 3.2+. Use the new client API shown above instead.

```csharp
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Parameters;
using Microsoft.Extensions.DependencyInjection;

var workflowRuntime = serviceProvider.GetRequiredService<IWorkflowRuntime>();

// Start a workflow with input parameters (obsolete API)
var result = await workflowRuntime.StartWorkflowAsync(
    "my-workflow",
    new StartWorkflowRuntimeParams
    {
        Input = new Dictionary<string, object>
        {
            ["message"] = "Hello!",
            ["userId"] = 123
        },
        CorrelationId = "my-correlation-id"
    });

Console.WriteLine($"Workflow status: {result.WorkflowState.Status}");
```

### Comparison: IWorkflowRunner vs IWorkflowRuntime vs IWorkflowDispatcher

Understanding when to use each workflow execution service is important for designing your application architecture:

| Feature                   | IWorkflowRunner              | IWorkflowRuntime              | IWorkflowDispatcher                        |
| ------------------------- | ---------------------------- | ----------------------------- | ------------------------------------------ |
| **Execution Model**       | Synchronous, in-process      | Asynchronous with persistence | Queue-based dispatching                    |
| **Use Case**              | Unit tests, simple workflows | Most application scenarios    | Background processing, distributed systems |
| **Persistence**           | No (in-memory only)          | Yes                           | Yes (via runtime)                          |
| **State Management**      | Transient                    | Full state tracking           | Managed by runtime                         |
| **Resumption Support**    | No                           | Yes                           | Yes                                        |
| **Bookmark Support**      | Limited                      | Full                          | Full                                       |
| **Distributed Execution** | No                           | Limited                       | Yes                                        |
| **API Complexity**        | Simple                       | Moderate                      | Advanced                                   |
| **Typical Namespace**     | `Elsa.Workflows`             | `Elsa.Workflows.Runtime`      | `Elsa.Workflows.Runtime`                   |

#### When to Use Each

**Use IWorkflowRunner when:**

* Writing unit tests for workflow logic
* Executing simple, short-lived workflows that don't need persistence
* Running workflows entirely in-process without external dependencies
* You need immediate, synchronous execution

**Use IWorkflowRuntime when:**

* Building applications that need workflow persistence and state management
* You need to resume workflows after suspension (bookmarks, delays)
* You want the high-level client API for workflow operations
* Most production scenarios with standard execution requirements

**Use IWorkflowDispatcher when:**

* Implementing custom workflow execution strategies
* Building queue-based or message-driven workflow systems
* Creating distributed workflow architectures across multiple nodes
* You need fine-grained control over workflow dispatching and execution

For more details on the dispatcher architecture, see the [Workflow Dispatcher Guide](/guides/architecture/workflow-dispatcher).


# Using Elsa Studio

The easiest way to start a workflow is directly from [Elsa Studio](/getting-started/containers/docker#elsa-server-and-studio).

<figure><img src="/files/yXO3WqzwTKEavJJPR4cU" alt=""><figcaption></figcaption></figure>

From the designer, click the green arrow to start the workflow.


# Altering a Running Workflow Instance

Choose the right Elsa 3.8.0 alteration path, apply changes safely, and monitor the resulting workflow and alteration jobs.

Alterations change the state of an existing workflow instance without changing the workflow definition for every instance. They are useful for operational corrections, controlled recovery, and migrating a running instance to another version of the same definition.

This guide describes the behavior of Elsa `release/3.8.0`. For the individual request and extension contracts, see the [Alterations feature reference](/features/alterations).

## Choose an execution path

Start with the target-selection and auditability requirements:

* **Known instance IDs:** use `POST /alterations/run` or `IAlterationRunner`. You get one result per requested instance; the HTTP endpoint also resumes successful instances that have scheduled work.
* **Filtered or auditable changes:** use `POST /alterations/dry-run`, then `POST /alterations/submit`. Elsa creates an asynchronous plan and one job for each matched instance.
* **One visible instance in Studio:** use **Alterations → Instances**. Studio stages a plan for the selected instance and provides plan/job inspection.
* **Retrying faulted work:** use `GET` or `POST /alterations/workflows/retry`. Elsa creates `ScheduleActivity` alterations and dispatches the instance.

Use a plan when the target set is defined by runtime state, such as a workflow definition, incident state, activity, or time range. Use immediate execution when the target set is already known and the caller needs the alteration log synchronously.

## Enable the feature

Register the Core Alterations module on the Elsa Server:

```csharp
builder.Services.AddElsa(elsa =>
{
    elsa.UseAlterations();
});
```

The module registers the five built-in alteration types:

* `ModifyVariable` changes an existing variable by variable ID.
* `ScheduleActivity` schedules an activity by activity ID or activity-instance ID.
* `CancelActivity` cancels one or more running activity instances.
* `Cancel` cancels the workflow instance.
* `Migrate` loads a specific version of the same workflow definition.

The server endpoints require `run:alterations` for writes and dry runs, and `read:alterations` to retrieve a stored plan and its jobs. The Studio module also needs to be enabled and connected to the server's Alterations feature.

## Apply a known change immediately

Use `POST /alterations/run` when you already have the workflow instance IDs. The request applies the alterations to each requested instance and returns a `RunAlterationsResult` containing the instance ID, log, success flag, and whether the updated workflow has scheduled work.

For example, the following changes a variable and schedules an activity:

```http
POST /alterations/run HTTP/1.1
Host: localhost:5001
Content-Type: application/json

{
  "workflowInstanceIds": ["<workflow-instance-id>"],
  "alterations": [
    {
      "type": "ModifyVariable",
      "variableId": "<variable-id>",
      "value": "approved"
    },
    {
      "type": "ScheduleActivity",
      "activityId": "ReviewOrder"
    }
  ]
}
```

The HTTP endpoint dispatches successful results that contain scheduled work. If you call `IAlterationRunner` directly, it only applies and commits the state. Call `IAlteredWorkflowDispatcher` yourself when the result has scheduled work:

```csharp
var results = await runner.RunAsync(
    workflowInstanceIds,
    alterations,
    cancellationToken);

await alteredWorkflowDispatcher.DispatchAsync(results, cancellationToken);
```

An alteration can fail for an individual instance—for example, when a variable, activity, or target workflow version does not exist. Inspect the result log before treating the operation as complete.

## Use a plan for filtered or auditable changes

Plans are asynchronous. Submission dispatches Elsa's system workflow `Elsa.Alterations.ExecuteAlterationPlan`, which stores the plan, finds matching instances, creates one alteration job per match, and dispatches those jobs. The plan and jobs have separate statuses and logs, so you can distinguish "the plan found no targets" from "one targeted instance failed".

For a broad filter, first run a dry run:

```http
POST /alterations/dry-run HTTP/1.1
Host: localhost:5001
Content-Type: application/json

{
  "definitionIds": ["order-processing"],
  "statuses": ["Running"],
  "hasIncidents": true,
  "isSystem": false
}
```

The response contains the workflow instance IDs that the filter would select. If the result is correct, submit the same filter with the alterations:

```http
POST /alterations/submit HTTP/1.1
Host: localhost:5001
Content-Type: application/json

{
  "alterations": [
    {
      "type": "ScheduleActivity",
      "activityId": "ReviewOrder"
    }
  ],
  "filter": {
    "workflowInstanceIds": ["<confirmed-instance-id>"]
  }
}
```

The submission response contains the plan ID. Retrieve its current plan and jobs with:

```http
GET /alterations/<plan-id> HTTP/1.1
Host: localhost:5001
```

An empty match is still a valid plan: Elsa stores the plan, creates no jobs, and completes the plan through the no-job path. Use `read:alterations` to inspect the plan, job status, timestamps, and per-job log entries.

## Use the Studio workflow

With the server and Studio Alterations modules enabled:

1. Open **Alterations → Instances**. Studio lists running, non-system workflow instances and provides an **Alter** action for each one.
2. Stage one or more of the five built-in alterations for that instance.
3. Submit the staged plan.
4. Open **Alterations → Plans** and select the plan to inspect its status, generated jobs, target instance links, and log entries.

Studio's staging flow is intentionally instance-oriented. It does not replace the filter authoring and dry-run workflow for bulk operations; use the server API for those operations and Studio's Plans view for follow-up inspection.

## Configure durability and background execution

`UseAlterations()` uses in-memory plan and job stores and an in-memory job dispatcher unless you replace them. These defaults are suitable for local development, but plan and job records and queued jobs are not durable across a process restart.

For durable plan and job records, configure an available persistence provider inside `UseAlterations`:

```csharp
builder.Services.AddElsa(elsa =>
{
    elsa.UseAlterations(alterations =>
    {
        alterations.UseEntityFrameworkCore(ef =>
            ef.UseSqlServer(connectionString));
    });
});
```

The `elsa-extensions` repository also provides MongoDB persistence and a MassTransit alteration-job dispatcher:

```csharp
elsa.UseAlterations(alterations =>
{
    alterations.UseMongoDb();
    alterations.UseMassTransitDispatcher();
});
```

The MassTransit dispatcher changes how generated alteration jobs are sent to a consumer. It does not change the synchronous `/alterations/run` path. In a multi-node deployment, install the same alteration handlers and workflow definition versions on every node that can load or resume the affected instances, and use durable stores and broker topology appropriate for the failure model.

## Retry faulted activities

Use the retry endpoint when the intent is specifically to retry faulted work. If `activityIds` is omitted, Elsa reads the incident activity IDs from each target workflow instance. If it is supplied, Elsa schedules only those activity IDs.

```http
POST /alterations/workflows/retry HTTP/1.1
Host: localhost:5001
Content-Type: application/json

{
  "workflowInstanceIds": ["<workflow-instance-id>"],
  "activityIds": ["CapturePayment"]
}
```

In `release/3.8.0`, the endpoint accepts both `GET` and `POST`. Send one workflow instance ID per request: the released handler loops over the loaded instances but passes the full request ID collection to the alteration runner, which can repeat work and result entries when several IDs are batched.

## Add a custom alteration

When the change cannot be expressed with the built-in types, implement `IAlteration` and an `IAlterationHandler` (or derive from `AlterationHandlerBase<T>`), then register the pair with `AddAlteration<T, THandler>()`. The handler runs against the existing workflow execution context and must explicitly succeed or fail the operation. See [Alteration extensibility](/features/alterations/applying-alterations/extensibility) for the implementation contract.

## Operational checklist

* Confirm the target IDs or dry-run filter results before changing state.
* Use `run:alterations` and `read:alterations` as separate least-privilege capabilities.
* Test a representative instance before submitting a broad plan.
* Keep variable IDs, activity IDs, and workflow definition versions stable enough for the persisted instances being operated on.
* Use durable plan/job storage and a durable job dispatcher when a restart or node failure must not lose alteration work.
* Review plan status, job status, and log entries; a submitted plan is not the same thing as a successful alteration on every target.

## Release source

This page was checked against the following `release/3.8.0` implementations:

* [Core Alterations feature](https://github.com/elsa-workflows/elsa-core/blob/edb5f7cd51e1c24a6ccbbe215684661e3d6c1e33/src/modules/Elsa.Alterations/Features/AlterationsFeature.cs)
* [Core immediate execution endpoint](https://github.com/elsa-workflows/elsa-core/blob/edb5f7cd51e1c24a6ccbbe215684661e3d6c1e33/src/modules/Elsa.Alterations/Endpoints/Alterations/Run/Endpoint.cs)
* [Core alteration runner](https://github.com/elsa-workflows/elsa-core/blob/edb5f7cd51e1c24a6ccbbe215684661e3d6c1e33/src/modules/Elsa.Alterations/Services/DefaultAlterationRunner.cs)
* [Core alteration-plan scheduler](https://github.com/elsa-workflows/elsa-core/blob/edb5f7cd51e1c24a6ccbbe215684661e3d6c1e33/src/modules/Elsa.Alterations/Services/DefaultAlterationPlanScheduler.cs)
* [Core alteration-plan workflow](https://github.com/elsa-workflows/elsa-core/blob/edb5f7cd51e1c24a6ccbbe215684661e3d6c1e33/src/modules/Elsa.Alterations/Workflows/ExecuteAlterationPlanWorkflow.cs)
* [Core retry endpoint](https://github.com/elsa-workflows/elsa-core/blob/edb5f7cd51e1c24a6ccbbe215684661e3d6c1e33/src/modules/Elsa.Alterations/Endpoints/Workflows/Retry/Endpoint.cs)
* [Core alteration filter](https://github.com/elsa-workflows/elsa-core/blob/edb5f7cd51e1c24a6ccbbe215684661e3d6c1e33/src/modules/Elsa.Alterations.Core/Models/AlterationWorkflowInstanceFilter.cs)
* [Studio Alterations menu](https://github.com/elsa-workflows/elsa-studio/blob/ef6a39d103d1a76e9f33fd4a37f499c5f02a4bfa/src/modules/Elsa.Studio.Alterations/Menu/AlterationsMenu.cs)
* [Studio instance list](https://github.com/elsa-workflows/elsa-studio/blob/ef6a39d103d1a76e9f33fd4a37f499c5f02a4bfa/src/modules/Elsa.Studio.Alterations/Pages/Instances/Index.razor)
* [Studio plan details](https://github.com/elsa-workflows/elsa-studio/blob/ef6a39d103d1a76e9f33fd4a37f499c5f02a4bfa/src/modules/Elsa.Studio.Alterations/Pages/Plans/Details.razor)
* [Extensions MongoDB alteration persistence](https://github.com/elsa-workflows/elsa-extensions/blob/d407e9621770a55427ac6c2315bd779da08d5fea/src/modules/persistence/Elsa.Persistence.MongoDb/Modules/Alterations/Extensions.cs)
* [Extensions MassTransit alteration dispatcher](https://github.com/elsa-workflows/elsa-extensions/blob/d407e9621770a55427ac6c2315bd779da08d5fea/src/modules/alterations/Elsa.Alterations.MassTransit/Extensions/ModuleExtensions.cs)


# Using a Trigger

Another way to run a workflow is through a trigger.

A trigger is represented by an activity, which provides trigger details to services external to the workflow that are ultimately responsible for triggering the workflow.

Elsa ships with various triggers out of the box, such as:

* HTTP Endpoint: triggers the workflow when a given HTTP request is sent to the workflow server.
* Timer: triggers the workflow on a fixed recurring interval based on a `TimeSpan` expression.
* Cron: triggers the workflow on a recurring cron schedule.
* StartAt: triggers the workflow once at a specific future timestamp.
* Event: triggers when a given event is received by the workflow server.

For scheduling-focused guidance, including `Delay`, `StartAt`, `Timer`, `Cron`, scheduler behavior, and clustered deployment considerations, see [Timer and Scheduled Workflows](/guides/running-workflows/timer-and-scheduled-workflows).

We will use the HTTP Endpoint trigger as an example.

## Using Code

The following code listing demonstrates a simple workflow using an HTTP Endpoint as its trigger.

{% code title="HelloWorldHttpWorkflow\.cs" %}

```csharp
using System.Net;
using Elsa.Http;
using Elsa.Workflows;
using Elsa.Workflows.Activities;

namespace Elsa.Samples.AspNet.HelloWorld;

public class HelloWorldHttpWorkflow : WorkflowBase
{
    protected override void Build(IWorkflowBuilder builder)
    {
        builder.Root = new Sequence
        {
            Activities =
            {
                new HttpEndpoint
                {
                    Path = new("/hello-world"),
                    SupportedMethods = new([HttpMethods.Get]),
                    CanStartWorkflow = true
                },
                new WriteHttpResponse
                {
                    StatusCode = new(HttpStatusCode.OK),
                    Content = new("Hello world!")
                }
            }
        };
    }
}
```

{% endcode %}

## Using Elsa Studio

Follow this guide to see step-by-step how to create a simple HTTP workflow using the HTTP Endpoint trigger.

{% embed url="<https://dubble.so/guides/http-trigger-zuc6xtxdvoo49omo5oly>" %}


# Timer and Scheduled Workflows

Elsa 3.8.0 provides four built-in scheduling activities that cover most time-based workflow patterns:

* `Delay`: pause a running workflow and resume it later.
* `Timer`: start a workflow repeatedly at a fixed interval, or wait for an interval inside a running workflow.
* `Cron`: start a workflow repeatedly from a cron schedule, or wait for the next cron occurrence inside a running workflow.
* `StartAt`: start a workflow once at a specific timestamp, or continue immediately if that timestamp is already in the past.

Use this guide when you need reminders, polling jobs, recurring background processes, or workflows that wait before continuing.

## Choose the right activity

| Need                                                                      | Activity  |
| ------------------------------------------------------------------------- | --------- |
| Pause the current workflow for 5 minutes, 2 hours, or 1 day               | `Delay`   |
| Run a workflow every fixed interval, such as every 15 minutes             | `Timer`   |
| Run a workflow on a calendar schedule, such as every weekday at 09:00 UTC | `Cron`    |
| Run a workflow once at a known future timestamp                           | `StartAt` |

The main distinction is this:

* `Delay` is for resuming an existing workflow instance.
* `Timer`, `Cron`, and `StartAt` can act as workflow triggers when `CanStartWorkflow` is enabled.

## How scheduling works in Elsa 3.8.0

At the application level, scheduling is enabled with `UseScheduling()`:

```csharp
builder.Services.AddElsa(elsa =>
{
    elsa.UseScheduling();
});
```

In `release/3.8.0`, `UseScheduling()` wires Elsa to the default local scheduler, which is an in-memory, in-process scheduler. That is fine for local development and single-node deployments, but it is not the right operational model for durable multi-node timer execution.

Under the hood, Elsa uses two scheduling paths:

* `DefaultTriggerScheduler` schedules trigger activities from published workflow definitions.
* `DefaultBookmarkScheduler` schedules bookmarks that resume existing workflow instances.

That distinction is important:

* triggers start new workflow instances
* bookmarks resume existing workflow instances

For clustered scheduled workloads, follow the patterns in the [Clustering guide](/guides/clustering), especially the Quartz-based scheduler pattern and the single-scheduler-node pattern.

## Starting workflows on a schedule

When `Timer`, `Cron`, or `StartAt` should create new workflow instances, the activity must be indexed as a trigger. In practice, that means the activity needs `CanStartWorkflow = true`.

### Timer trigger

Use `Timer` when you want a workflow to start repeatedly at a fixed interval.

```csharp
using Elsa.Scheduling.Activities;
using Elsa.Workflows;
using Elsa.Workflows.Activities;

public class RecurringCleanupWorkflow : WorkflowBase
{
    protected override void Build(IWorkflowBuilder builder)
    {
        builder.Root = new Sequence
        {
            Activities =
            {
                new Timer(TimeSpan.FromMinutes(15))
                {
                    CanStartWorkflow = true
                },
                new WriteLine("Running scheduled cleanup")
            }
        };
    }
}
```

In 3.8.0, Elsa indexes a timer trigger by calculating `StartAt = UtcNow + Interval` at trigger-index time. In practice, the first run is relative to when the workflow definition is published or re-indexed, not relative to a fixed wall-clock time.

The public input name is `Interval`.

### Cron trigger

Use `Cron` when the schedule must follow calendar rules instead of a simple interval.

```csharp
using Elsa.Scheduling.Activities;
using Elsa.Workflows;
using Elsa.Workflows.Activities;

public class WeekdayReportWorkflow : WorkflowBase
{
    protected override void Build(IWorkflowBuilder builder)
    {
        builder.Root = new Sequence
        {
            Activities =
            {
                new Cron("0 0 9 * * MON-FRI")
                {
                    CanStartWorkflow = true
                },
                new WriteLine("Generating weekday report")
            }
        };
    }
}
```

Elsa 3.8.0 validates cron expressions through the `Cronos` parser using the six-field format with seconds. For example:

* `0 0 9 * * MON-FRI` means 09:00:00 UTC on weekdays.
* `0 */15 * * * *` means every 15 minutes.

When you replace the local scheduler with Hangfire, Elsa still validates a published cron trigger with its Cronos parser and then passes the expression to Hangfire's recurring-job manager. Make sure it satisfies both systems; do not assume the local scheduler's six-field parser rules apply unchanged. See [Hangfire Integration](/guides/running-workflows/hangfire-integration).

By default in 3.8.0, invalid cron expressions block publishing because workflow publishing fails on validation errors. If you intentionally want publishing to continue while surfacing validation warnings, disable that behavior in workflow management options:

```csharp
builder.Services.AddElsa(elsa =>
{
    elsa.UseWorkflowManagement(management => management.UseFailOnValidationErrors(false));
});
```

The public input name is `CronExpression`.

### StartAt trigger

Use `StartAt` when the workflow should start once at a specific future timestamp.

```csharp
using Elsa.Scheduling.Activities;
using Elsa.Workflows;
using Elsa.Workflows.Activities;

public class LaunchWorkflow : WorkflowBase
{
    protected override void Build(IWorkflowBuilder builder)
    {
        builder.Root = new Sequence
        {
            Activities =
            {
                new StartAt(new DateTimeOffset(2026, 7, 1, 8, 0, 0, TimeSpan.Zero))
                {
                    CanStartWorkflow = true
                },
                new WriteLine("Launch window opened")
            }
        };
    }
}
```

If a stored `StartAt` trigger is already in the past when Elsa schedules it, Elsa still schedules a catch-up execution. Inside an already running workflow instance, however, `StartAt` completes immediately when the configured time is in the past or equal to now.

The public input name is `DateTime`.

## Waiting inside a running workflow

### Delay

Use `Delay` to suspend an existing workflow instance and resume it later.

```csharp
using Elsa.Scheduling.Activities;
using Elsa.Workflows;
using Elsa.Workflows.Activities;

public class FollowUpWorkflow : WorkflowBase
{
    protected override void Build(IWorkflowBuilder builder)
    {
        builder.Root = new Sequence
        {
            Activities =
            {
                new WriteLine("Initial work"),
                Delay.FromHours(2),
                new WriteLine("Continue after the delay")
            }
        };
    }
}
```

`Delay` creates a bookmark with a specific resume time. It does not start new workflow instances by itself.

In 3.8.0, the public input name is `TimeSpan`. Older examples that show `Duration` are not correct for Elsa 3.8.

### Timer and Cron inside a running workflow

`Timer` and `Cron` can also be used inside a running workflow instead of only at the start.

* `Timer` waits for the configured interval, then continues.
* `Cron` waits until the next matching cron occurrence, then continues.

That makes them useful for recurring loops, polling, and wait-until-next-window patterns where the workflow instance should keep its state between resumptions.

In 3.8.0, `DefaultBookmarkScheduler` schedules:

* `Delay`, `Timer`, and `StartAt` bookmarks with `ScheduleAtAsync(...)`
* `Cron` bookmarks with `ScheduleCronAsync(...)`

## Using Elsa Studio

In Elsa Studio, these activities are available from the scheduling/toolbox categories exposed by the server's registered activities.

For schedule-driven workflows:

1. Add `Timer`, `Cron`, or `StartAt` near the beginning of the workflow.
2. In the activity properties, enable `CanStartWorkflow`.
3. Publish the workflow so Elsa can index and schedule the trigger.
4. Verify executions from the workflow instances view or logs.

For pause-and-resume workflows:

1. Add a `Delay` activity where the workflow should pause.
2. Configure the delay value.
3. Publish and run the workflow.
4. Inspect the suspended instance if you need to confirm it is waiting on scheduled work.

If scheduled workflows do not fire, check the [Troubleshooting guide](/guides/troubleshooting) and the [Clustering guide](/guides/clustering) before assuming the activity configuration is wrong.

## Durable scheduler options

If you need schedules to survive restarts or coordinate across multiple nodes, replace the default local scheduler.

Quartz is the built-in durable option most Elsa deployments use:

```csharp
builder.Services.AddElsa(elsa =>
{
    elsa.UseScheduling(scheduling => scheduling.UseQuartzScheduler());
    elsa.UseQuartz(quartz => quartz.UsePostgreSql(connectionString));
});
```

In `release/3.8.0`, Quartz replaces the scheduling feature's `WorkflowScheduler` with `QuartzWorkflowScheduler` and swaps the cron parser to `QuartzCronParser`.

Hangfire is also supported:

```csharp
using Hangfire.SqlServer;

builder.Services.AddElsa(elsa =>
{
    elsa.UseHangfire(hangfire => hangfire.UseJobStorage(new SqlServerStorage(connectionString)));
    elsa.UseScheduling(scheduling => scheduling.UseHangfireScheduler());
});
```

In `release/3.8.0`, Hangfire replaces the scheduling feature's `WorkflowScheduler` with `HangfireWorkflowScheduler`. For durable storage, worker configuration, operational boundaries, and verification, see [Hangfire Integration](/guides/running-workflows/hangfire-integration).

## Operational notes

Keep these 3.8.0 behaviors in mind:

* `Timer`, `Cron`, and `StartAt` only become workflow-starting triggers when `CanStartWorkflow` is enabled.
* `Delay` always resumes an existing workflow instance; it is not a start trigger.
* `Timer` schedules its first trigger occurrence relative to trigger indexing time.
* `Cron` uses the Cronos parser with seconds included.
* `StartAt` catches up trigger executions that are already in the past when Elsa schedules them.
* `UseScheduling()` configures the default local in-memory scheduler.
* Scheduled bookmarks for `Delay`, `Timer`, `Cron`, and `StartAt` are all handed to Elsa's workflow scheduler, so the deployment model determines whether scheduled execution is local-only or suitable for clustered workloads.

## Related guides

* [Using a Trigger](/guides/running-workflows/using-a-trigger)
* [Running Workflows](/guides/running-workflows)
* [Long-Running Workflows](/guides/running-workflows/long-running-workflows)
* [Clustering](/guides/clustering)
* [Troubleshooting](/guides/troubleshooting)


# Workflow Definition Version Lifecycle

Elsa keeps a workflow definition's stable **definition ID** separate from the individual **version ID** and numeric **version**. This lets a designer save a new draft without changing the identity used by callers, while operators can choose whether an API operation uses the latest, published, draft, or a specific version.

This page describes the release `3.8.0` behavior and how it appears in Elsa Studio.

## The three identities to keep straight

| Value                        | Meaning                                    | Changes when you save a new version? |
| ---------------------------- | ------------------------------------------ | ------------------------------------ |
| `DefinitionId`               | Stable identity of the workflow definition | No                                   |
| `Id` / definition version ID | Identity of one stored version             | Yes                                  |
| `Version`                    | Human-readable, increasing version number  | Yes                                  |

Each stored version also has `IsLatest` and `IsPublished` flags. The latest version is the current editable version. The published version is the version used by default when the REST API starts a workflow.

Do not treat a version number as the definition ID. Use the definition ID to address the workflow family, and use a version option or version ID when you need a particular stored version.

## Lifecycle at a glance

```
New or edit draft
      |
      v
Save draft  --->  latest, not published
      |
      +---- Publish ---> latest + published
      |                         |
      |                         +---- Edit ---> new latest draft
      |                         |
      |                         +---- Unpublish/retract ---> latest, not published
      |
      +---- Roll back a history entry ---> new latest draft copied from that version
```

Publishing a new version does not mutate the previous version's workflow graph. It moves the published marker to the selected latest version and retracts the previous published version. Retraction only clears the published state; it does not delete the version.

## Drafts, latest versions, and publishing

When Elsa creates a workflow definition, it starts as version 1, marked latest and not published. Saving a draft keeps it latest. When a published definition is edited, Elsa creates a shallow copy with a new version ID and the next version number, then marks that copy as the latest unpublished draft.

Saving the same draft record does not necessarily create another version on every save. A new record receives the next version number; saving the existing latest record preserves its version number.

Publishing performs validation first. If validation succeeds, Elsa:

1. clears `IsLatest` and `IsPublished` on the previous latest/published records;
2. marks the selected latest version as both latest and published; and
3. emits the publish/retract notifications used by runtime and integration features.

If validation is configured to fail the publish, no new published state is written. A publish operation therefore means “validate and make this latest version active”, not “create a separate immutable copy”.

## Which version is selected?

Core exposes these selectors through `VersionOptions`:

| Selector             | Use                                                    |
| -------------------- | ------------------------------------------------------ |
| `Latest`             | Current latest version, including an unpublished draft |
| `Published`          | Current published version only                         |
| `LatestOrPublished`  | Latest when available, otherwise published             |
| `LatestAndPublished` | Latest version only when it is also published          |
| `Draft`              | An unpublished version                                 |
| `AllVersions`        | Complete version history                               |
| a number such as `3` | One specific numeric version                           |

The default matters:

* REST `GET`/`POST /workflow-definitions/{definitionId}/execute` and `POST /workflow-definitions/{definitionId}/dispatch` use `Published` when the request omits `versionOptions`.
* The Studio workflow list's **Run** action explicitly uses `Latest`, so it can run the current draft/latest version.
* Studio's workflow editor loads `Latest` for editing and its **Version history** tab loads `AllVersions` for inspection.

For production callers, leave the REST default in place unless you have a deliberate reason to run a draft or pin a specific version.

### Select a version in the REST API

The read endpoints accept `versionOptions` as a query-string value. The version list is ordered from newest to oldest:

```http
GET /elsa/api/workflow-definitions/order-approval?versionOptions=Published
GET /elsa/api/workflow-definitions/order-approval?versionOptions=3
GET /elsa/api/workflow-definitions/order-approval/versions
```

Execution and dispatch accept the same selector in the JSON body. A numeric JSON value selects that version; the string form is also accepted by Elsa's JSON converter:

```http
POST /elsa/api/workflow-definitions/order-approval/dispatch
Content-Type: application/json
Authorization: ApiKey YOUR_API_KEY

{
  "versionOptions": 3,
  "input": {
    "orderId": "A-1042"
  }
}
```

Use a specific version for controlled replay or compatibility testing. Avoid pinning a version in normal business traffic unless the caller owns the upgrade policy; otherwise published-version promotion remains the deployment switch.

## Publish and retract from Studio

In the Studio workflow list, the table shows **Latest version** and **Published version** separately. A dash in the published column means that no version is currently published. The row menu and bulk-actions menu provide **Publish** and **Unpublish** actions when the backend exposes the required links and the application is not in read-only mode.

Inside the editor:

* use **Save** or enable **Auto-save** to persist the current draft;
* use **Publish workflow** to validate and publish the latest version; and
* use the editor menu's **Unpublish** action to retract the current published version.

Publishing can also update workflows that consume the published definition. Studio reports the count when the backend returns affected consuming workflows. This is separate from migrating already-running instances; use the [alterations guide](/features/alterations) when existing instances must move to a newer published version.

## Inspect and roll back version history

Open a workflow in Studio and use the **Version history** section in the workflow properties. Studio loads every stored version and shows whether each one is published, its version number, and its creation time. From a history entry you can:

* **View** the stored version without replacing the current latest version;
* **Delete** a version when editing is allowed and more than one version remains; or
* choose **Rollback to this version** for a non-latest version.

Rollback is implemented as a new version. Elsa copies the selected historical definition into a new record, assigns the next version number, and marks that record latest. The historical record remains available. Rollback does not publish the new version automatically; publish it after reviewing the copied draft.

The rollback endpoint is:

```http
POST /elsa/api/workflow-definitions/{definitionId}/revert/{version}
```

It uses the `publish:workflow-definitions` permission and returns the newly created version summary. This endpoint is a version-history operation, not a runtime migration. If instances already run an older version, plan their migration separately with alterations.

## API operations and permissions

The main lifecycle operations are:

| Operation                    | Endpoint                                                                             | Permission                     |
| ---------------------------- | ------------------------------------------------------------------------------------ | ------------------------------ |
| List definitions or versions | `GET /workflow-definitions` and `GET /workflow-definitions/{definitionId}/versions`  | `read:workflow-definitions`    |
| Save a draft                 | `POST /workflow-definitions` or the version update endpoint exposed by the API links | `write:workflow-definitions`   |
| Publish latest               | `POST /workflow-definitions/{definitionId}/publish`                                  | `publish:workflow-definitions` |
| Retract published version    | `POST /workflow-definitions/{definitionId}/retract`                                  | `retract:workflow-definitions` |
| Roll back to a version       | `POST /workflow-definitions/{definitionId}/revert/{version}`                         | `publish:workflow-definitions` |
| Start a workflow             | `GET`/`POST .../execute` or `POST .../dispatch`                                      | `exec:workflow-definitions`    |

Use the API's hypermedia links to determine whether a specific definition is read-only or whether an operation is available. Studio uses those links to disable edit, publish, retract, and delete actions; a user seeing a missing action may be in read-only mode or lack the corresponding permission.

## Deployment guidance

Treat publishing as the boundary between design-time and runtime use:

1. edit and test the latest draft in Studio;
2. inspect the version number and definition ID;
3. publish after validation succeeds;
4. verify that the published-version column shows the intended version; and
5. let normal REST callers use the published default.

If you need to test an unpublished version through an API, make the selector explicit and keep that path out of production traffic. If you need to change the behavior of instances that are already running, use an alteration plan or targeted alteration rather than assuming that publishing changes their stored definition version.

## Release source

The behavior on this page is based on the current `release/3.8.0` refs:

* [Core workflow definition publisher](https://github.com/elsa-workflows/elsa-core/blob/f1e2a092f916c41d1949bf36efa16a90abda664d/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionPublisher.cs)
* [Core version selectors](https://github.com/elsa-workflows/elsa-core/blob/f1e2a092f916c41d1949bf36efa16a90abda664d/src/clients/Elsa.Api.Client/Shared/Models/VersionOptions.cs)
* [Core execute version default](https://github.com/elsa-workflows/elsa-core/blob/f1e2a092f916c41d1949bf36efa16a90abda664d/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/WorkflowExecutionHelper.cs)
* [Core publish endpoint](https://github.com/elsa-workflows/elsa-core/blob/f1e2a092f916c41d1949bf36efa16a90abda664d/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Endpoint.cs)
* [Core rollback endpoint](https://github.com/elsa-workflows/elsa-core/blob/f1e2a092f916c41d1949bf36efa16a90abda664d/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/Revert.cs)
* [Studio workflow list](https://github.com/elsa-workflows/elsa-studio/blob/release/3.8.0/src/modules/Elsa.Studio.Workflows/Components/WorkflowDefinitionList/WorkflowDefinitionList.razor)
* [Studio version history](https://github.com/elsa-workflows/elsa-studio/blob/release/3.8.0/src/modules/Elsa.Studio.Workflows/Components/WorkflowDefinitionEditor/Components/WorkflowProperties/Tabs/VersionHistory/VersionHistoryTab.razor.cs)


# Hangfire Integration

Configure Hangfire as Elsa's durable workflow scheduler.

Use Hangfire when Elsa's scheduled workflow work must be stored outside the application process and executed by Hangfire workers. It is a scheduler replacement for `Delay`, `Timer`, `Cron`, and `StartAt` work; it does not replace Elsa workflow persistence, workflow dispatching, or clustering setup.

For a single-node development host, `UseScheduling()` uses Elsa's local, in-memory scheduler. Choose Hangfire when you already operate Hangfire and want its persistent storage and worker model for Elsa's scheduled workflow jobs.

## What the integration schedules

After you enable `UseHangfireScheduler()`, Elsa replaces the scheduling feature's `IWorkflowScheduler` implementation with `HangfireWorkflowScheduler`.

| Elsa schedule                                                   | Hangfire job                    |
| --------------------------------------------------------------- | ------------------------------- |
| A future start of a new workflow instance                       | A scheduled `RunWorkflowJob`    |
| A `Timer` or `Cron` trigger that starts workflow instances      | A recurring `RunWorkflowJob`    |
| A `Delay`, `Timer`, or `StartAt` bookmark in a running instance | A scheduled `ResumeWorkflowJob` |
| A `Cron` bookmark in a running instance                         | A recurring `ResumeWorkflowJob` |

The integration carries the current tenant ID into each job and restores that tenant context before it runs the workflow. Elsa's job handlers ask Hangfire to fail after its retry attempts are exhausted; decide your broader retry and incident strategy separately in [Error handling and retry logic](/operate/incidents).

## Configure Elsa and Hangfire

Install `Elsa.Scheduling.Hangfire` and the Hangfire storage provider you choose. For example, the SQL Server setup below also needs `Hangfire.SqlServer`.

```csharp
using Elsa.Extensions;
using Hangfire.SqlServer;

var connectionString = builder.Configuration.GetConnectionString("Hangfire")
    ?? throw new InvalidOperationException("Connection string 'Hangfire' is missing.");

builder.Services.AddElsa(elsa =>
{
    elsa.UseHangfire(hangfire =>
    {
        hangfire.UseJobStorage(new SqlServerStorage(connectionString));
    });

    elsa.UseScheduling(scheduling => scheduling.UseHangfireScheduler());
});
```

`UseHangfire(...)` registers Hangfire's services and background server. Elsa's defaults are one worker and a one-second schedule-polling interval. Keep those defaults while proving the integration, then tune them only against the capacity of the selected Hangfire storage and the cost of the workflows it will run.

```csharp
elsa.UseHangfire(hangfire =>
{
    hangfire.UseJobStorage(new SqlServerStorage(connectionString));
    hangfire.ConfigureBackgroundServerOptions((_, options) =>
    {
        options.WorkerCount = 4;
        options.SchedulePollingInterval = TimeSpan.FromSeconds(5);
    });
});
```

Do not call `UseHangfire(...)` when the host already configures `AddHangfire` and `AddHangfireServer` itself. The Elsa feature is intended to own that registration. In that case, wire Elsa's `IWorkflowScheduler` deliberately in your host instead of registering a second Hangfire server through Elsa.

## Choose and operate storage deliberately

`UseHangfire(...)` uses Hangfire memory storage when no storage is supplied. That is suitable only for local experimentation: scheduled jobs are lost when the process stops. Use a durable Hangfire storage implementation for restart survival or a multi-node deployment.

The released integration ships a general `UseJobStorage(JobStorage)` hook. Its older `UseSqlServerStorage(...)` and `UseSqliteStorage(...)` convenience APIs are obsolete; configure the storage directly on `HangfireFeature` instead, as in the example above. This also lets a host use another Hangfire storage provider without relying on an Elsa-specific wrapper.

When operating the system, monitor the same Hangfire queues that contain Elsa jobs. An Elsa unschedule request searches for matching scheduled jobs and jobs queued on Hangfire's `default` queue. It removes recurring `RunWorkflowJob` records, too. Do not treat that as a universal cleanup mechanism for every Hangfire queue or recurring resume job; verify cancellation behavior for your workflow and storage provider before relying on it operationally.

## Know the boundaries

* `UseHangfireScheduler()` affects Elsa's workflow scheduler. It does not make every background activity execute through Hangfire. The package has a separate `UseHangfireBackgroundActivityScheduler()` integration for hosts that intentionally use Elsa's `IBackgroundActivityScheduler`.
* The scheduler stores and executes work, but it is not a substitute for durable workflow state. Configure Elsa persistence separately.
* One-time and recurring job names originate from Elsa scheduler task names. Treat them as implementation identifiers, not as a stable dashboard-facing naming convention.
* Recurring registrations use Hangfire's `AddOrUpdate` operation. Reusing an Elsa scheduler task name replaces the existing recurring job. The `Timer` trigger's interval scheduler converts only day, hour, minute, and second components to cron and does not use its `startAt` argument. Enabling the Hangfire scheduler does not replace Elsa's Cronos parser: a published cron trigger must first pass Elsa validation, then be accepted by Hangfire when Elsa passes it to the recurring-job manager.
* Hangfire's storage, dashboard exposure, retention, authentication, and backup policies belong to your hosting and security design. Elsa does not configure those policies for you.

## Verify the integration

1. Configure durable Elsa persistence and a durable Hangfire job storage.
2. Publish a workflow with a future `StartAt`, `Timer`, or `Cron`; or publish and run a workflow that reaches a `Delay` bookmark.
3. Confirm the expected scheduled or recurring job appears in Hangfire.
4. Confirm an Elsa workflow instance starts or resumes at the expected time and under the correct tenant when multitenancy is enabled.
5. Restart one application node and confirm jobs remain in the shared Hangfire storage; then test the same workflow under the intended production worker topology.

## Related guides

* [Timer and Scheduled Workflows](/guides/running-workflows/timer-and-scheduled-workflows)
* [Long-Running Workflows](/guides/running-workflows/long-running-workflows)
* [Clustering](/guides/clustering)
* [Configuration Management](/guides/deployment/configuration-management)


# Long-Running Workflows

Use long-running workflows when work should pause and resume instead of finishing in a single request or execution burst. In Elsa `release/3.8.0`, that model is built on bookmarks, triggers, scheduling, queued stimuli, and runtime recovery.

This guide connects the pieces that are otherwise spread across the scheduling, running, clustering, and troubleshooting docs.

## What makes a workflow long-running

A workflow becomes long-running when it creates a wait point and Elsa persists enough runtime state to continue later. Typical wait points are:

* a scheduled pause such as `Delay`, inline `Timer`, inline `Cron`, or inline `StartAt`
* a callback wait such as an approval link or other bookmark-based resume
* a trigger or blocking activity waiting for external input such as HTTP, events, signals, or broker messages
* a background hand-off such as `RunTask`

The important distinction is whether the current workflow instance must survive beyond the current execution burst. If yes, design it as long-running from the start.

## Host capabilities you need

Long-running workflows usually need more than one Elsa module:

| Need                                | Required module                                             | Why it matters                                                         |
| ----------------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------- |
| Pause and resume existing instances | `UseWorkflowRuntime()`                                      | Stores bookmarks, resumes workflow instances, and runs recovery tasks  |
| Wait for future timestamps          | `UseScheduling()`                                           | Schedules resume work for `Delay`, `Timer`, `Cron`, and `StartAt`      |
| Survive process restarts            | persistent runtime storage                                  | Keeps workflow instances, bookmarks, and related runtime state durable |
| Run safely on multiple nodes        | `UseDistributedRuntime()` plus clustered scheduling/storage | Prevents competing resume work and coordinates background processing   |

At minimum, enable the workflow runtime:

```csharp
builder.Services.AddElsa(elsa => elsa
    .UseWorkflowRuntime());
```

If the workflow waits for future timestamps such as `Delay`, `Timer`, `Cron`, or `StartAt`, also enable scheduling and a runtime persistence provider:

```csharp
builder.Services.AddElsa(elsa => elsa
    .UseWorkflowRuntime(runtime =>
    {
        runtime.UseEntityFrameworkCore(ef => ef.UseSqlite());
        runtime.UseDistributedRuntime();
    })
    .UseScheduling());
```

Three practical rules matter for long-running workflows:

* use runtime persistence if instances, bookmarks, or queued resume work must survive process restarts
* use scheduling if workflows wait for future times
* use distributed runtime and clustered scheduling when multiple nodes can resume the same work

The sample server in `release/3.8.0` enables runtime persistence, `UseDistributedRuntime()`, and `UseScheduling()` together for exactly this reason.

## How Elsa pauses and resumes

When an activity creates a bookmark, Elsa records a wait condition for the current workflow instance and suspends execution after the current burst finishes.

| Concept  | What it does                                  | Typical examples                                                   |
| -------- | --------------------------------------------- | ------------------------------------------------------------------ |
| Bookmark | Pause point for an existing workflow instance | `Delay`, inline `Timer`, `Event`, `RunTask`                        |
| Trigger  | Start point for a workflow definition         | `HttpEndpoint`, trigger `Timer`, trigger `Cron`, trigger `StartAt` |
| Stimulus | Payload used to match a bookmark or trigger   | event name, HTTP route, timer payload, task ID                     |

In `release/3.8.0`, Elsa uses these runtime paths:

1. The activity writes a bookmark or trigger payload.
2. Elsa stores runtime state through the configured runtime store.
3. If the wait is time-based, Elsa schedules resume work through `IWorkflowScheduler`.
4. When the stimulus arrives, `WorkflowResumer` looks up matching bookmarks and acquires a distributed lock for the bookmark filter before resuming them.

That lock is why clustered resume operations do not rely on sticky sessions.

## Choose the right activation model

Use the smallest mechanism that matches the business event:

| Need                                      | Best fit                                  | Notes                                                              |
| ----------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------ |
| Wait for a duration                       | `Delay`                                   | Clearest one-shot pause                                            |
| Wait until a known timestamp              | `StartAt`                                 | Completes immediately if the timestamp is already in the past      |
| Continue at the next calendar match       | inline `Cron`                             | Resumes once, then continues                                       |
| Start a workflow on a schedule            | trigger `Timer`, `Cron`, or `StartAt`     | Starts a new workflow instance                                     |
| Wait for an app or user callback          | custom bookmark or tokenized bookmark URL | Good for approvals and external callbacks                          |
| Wait for a background task to report back | `RunTask`                                 | Creates a bookmark keyed by task ID                                |
| Wait for an external event or message     | trigger/blocking activity pair            | For example HTTP, Signal, Event, or MassTransit message activities |

## Pattern 1: wait for a future time

Use scheduling activities inline when the current workflow instance should continue later instead of starting a brand-new instance.

```csharp
using Elsa.Scheduling.Activities;
using Elsa.Workflows;
using Elsa.Workflows.Activities;

public class FollowUpWorkflow : WorkflowBase
{
    protected override void Build(IWorkflowBuilder builder)
    {
        builder.Root = new Sequence
        {
            Activities =
            {
                new WriteLine("Request received"),
                new Delay(TimeSpan.FromHours(4)),
                new WriteLine("Sending follow-up")
            }
        };
    }
}
```

In `release/3.8.0`:

* `Delay` calls `context.DelayFor(...)`, which creates a delay bookmark with a `ResumeAt` timestamp
* inline `Timer` also creates a one-shot bookmark
* inline `Cron` creates a bookmark for the next cron occurrence
* inline `StartAt` creates a bookmark only when the target time is in the future

The scheduling module then hands those bookmarks to `DefaultBookmarkScheduler`, which schedules resume work through `IWorkflowScheduler`.

## Pattern 2: start new workflow instances on a schedule

Use the same activities as triggers when the schedule should launch a new instance each time.

```csharp
new Timer(TimeSpan.FromMinutes(15))
{
    CanStartWorkflow = true
}
```

This is different from an inline timer:

* trigger `Timer` schedules recurring new-workflow starts
* trigger `Cron` schedules recurring new-workflow starts from the cron expression
* trigger `StartAt` schedules one future workflow start and logs a catch-up message if the configured time is already in the past
* inline scheduling activities wait inside the current workflow instance

In `release/3.8.0`, trigger schedules are created by `DefaultTriggerScheduler`, while inline waits are created by `DefaultBookmarkScheduler`.

For the scheduling-specific details, see [Timer and Scheduled Workflows](/guides/running-workflows/timer-and-scheduled-workflows).

## Pattern 3: wait for an external callback

For approvals, webhooks, and external hand-offs, create a bookmark and expose a resume URL or token.

```csharp
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;

[Activity("Custom", "Approvals", "Wait for an external approval callback.")]
public class WaitForApproval : Activity
{
    protected override ValueTask ExecuteAsync(ActivityExecutionContext context)
    {
        var bookmark = context.CreateBookmark(OnResumeAsync);
        var resumeUrl = context.GenerateBookmarkTriggerUrl(bookmark.Id, TimeSpan.FromDays(1));
        context.JournalData["ResumeUrl"] = resumeUrl;
        return ValueTask.CompletedTask;
    }

    private async ValueTask OnResumeAsync(ActivityExecutionContext context)
    {
        var decision = context.GetWorkflowInput<string>("Decision");
        await context.CompleteActivityWithOutcomesAsync(decision == "Approved" ? "Approved" : "Rejected");
    }
}
```

`GenerateBookmarkTriggerUrl(...)` comes from Elsa's HTTP integration, so use it when your host includes the HTTP module.

The built-in resume endpoint in `release/3.8.0` is:

* `GET {RoutePrefix}/bookmarks/resume?t=...`
* `POST {RoutePrefix}/bookmarks/resume?t=...`

With default API settings, that means `/elsa/api/bookmarks/resume?t=...`.

The endpoint also supports `async=true`, which enqueues bookmark resumption instead of resuming the workflow synchronously in the request.

Use the asynchronous form when the callback should return quickly or when the resume path might do meaningful work after the bookmark is matched.

## Pattern 4: wait for background work

`RunTask` is useful when the workflow asks the host application to do work outside the current execution path and continue later with a result.

In `release/3.8.0`, `RunTask`:

* generates a task ID
* creates a bookmark keyed by a `RunTaskStimulus`
* dispatches the task request through `ITaskDispatcher`
* resumes when the host reports back using that task stimulus

Use this when the workflow runtime should coordinate the task, but the task itself runs elsewhere.

## Resume paths you can rely on

In `release/3.8.0`, long-running workflows typically resume through one of four paths:

| Resume path              | Typical source                                           | What Elsa does                                                                      |
| ------------------------ | -------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| scheduled resume         | `Delay`, inline `Timer`, inline `Cron`, inline `StartAt` | Scheduler enqueues or executes resume work for an existing bookmark                 |
| bookmark resume endpoint | approval links, custom callback URLs                     | HTTP endpoint validates token and resumes immediately or enqueues with `async=true` |
| trigger dispatch         | HTTP, `Timer`, `Cron`, `StartAt`, message triggers       | Elsa starts a new workflow instance from a trigger                                  |
| custom stimulus dispatch | `RunTask`, events, signals, broker callbacks             | Elsa matches stored bookmarks or triggers from the incoming stimulus payload        |

## Dispatch vs execute

For long-running flows, prefer entry points that do not assume the workflow will finish in the same HTTP request.

* use `POST {RoutePrefix}/workflow-definitions/{definitionId}/dispatch` when the workflow may suspend or continue in the background
* use `GET` or `POST {RoutePrefix}/workflow-definitions/{definitionId}/execute` only when the caller expects a synchronous response and the workflow path can complete immediately

If an HTTP workflow can block on timers, callbacks, or external events, design it as a dispatch-and-observe flow instead of a request-response flow.

## Studio notes

In Elsa Studio, the same runtime distinction shows up through configuration:

* enable `Trigger Workflow` when an activity should start the workflow
* leave it disabled when the activity should pause the current path

That rule applies to built-in scheduling activities and to custom activities that can act as triggers.

## Operational notes

* Long-running durability depends on runtime persistence for workflow instances, bookmarks, triggers, and queued resume work.
* `TriggerBookmarkQueueRecurringTask` signals bookmark queue processing for queued bookmark resumes and other deferred bookmark stimuli.
* `PurgeBookmarkQueueRecurringTask` removes expired bookmark queue items based on `BookmarkQueuePurgeOptions`.
* `RestartInterruptedWorkflowsTask` looks for workflow instances that are still marked as executing but have been inactive longer than `RuntimeOptions.InactivityThreshold` and asks the runtime to restart them.
* For multi-node hosting, pair long-running workflows with the clustered guidance in [Clustering](/guides/clustering) and [Distributed Hosting](/hosting/distributed-hosting).

For operators, the most important runtime settings are:

* runtime persistence provider configuration
* distributed locking configuration when `UseDistributedRuntime()` is enabled
* `RuntimeOptions.InactivityThreshold` for interrupted workflow recovery
* recurring task schedules for bookmark queue triggering and purge
* API and HTTP ingress behavior if workflows are resumed through public or semi-public callback URLs

## Minimal operations checklist

Before calling a workflow long-running and production-ready, verify:

1. runtime persistence is configured for the workflow runtime
2. scheduling is enabled for any time-based waits
3. distributed runtime and shared backing stores are configured for multi-node hosting
4. resume endpoints or external callback handlers are authenticated or token-protected appropriately
5. operators know where to inspect blocked instances, incidents, and queued background work

## Common mistakes

* Using in-memory runtime storage for workflows that must survive restarts.
* Treating inline `Timer` or inline `Cron` as recurring loops. They are one-shot waits unless they start the workflow as triggers.
* Assuming a trigger activity and the same activity inline have the same runtime behavior.
* Expecting synchronous HTTP responses from workflows that can suspend.
* Forgetting clustered locking and scheduling when multiple nodes can process the same bookmarks.

## Related guides

* [Workflow Context](/getting-started/concepts/workflow-context)
* [Blocking Activities & Triggers](/activities/blocking-and-triggers)
* [Timer and Scheduled Workflows](/guides/running-workflows/timer-and-scheduled-workflows)
* [Running Workflows](/guides/running-workflows)
* [Clustering](/guides/clustering)


# Dispatch Workflow Activity

The **Dispatch Workflow** activity can start a new workflow from the current workflow.

It allows you to specify what workflow to run and provide any input required by the workflow.

If you need to fan out one child workflow per item in a collection, use [Bulk Dispatch Workflows Activity](/guides/running-workflows/bulk-dispatch-workflows) instead.

Let's try it out.

## Using Code

The following code listings show two workflows:

1. The parent workflow
2. The child workflow to dispatch

{% code title="ParentWorkflow\.cs" %}

```csharp
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Runtime.Activities;

public class ParentWorkflow : WorkflowBase
{
    protected override void Build(IWorkflowBuilder builder)
    {
        var childOutput = builder.WithVariable<IDictionary<string, object>>();

        builder.Root = new Sequence
        {
            Activities =
            {
                new DispatchWorkflow
                {
                    WorkflowDefinitionId = new(nameof(ChildWorkflow)),
                    Input = new(new Dictionary<string, object>
                    {
                        ["ParentMessage"] = "Hello from parent!"
                    }),
                    WaitForCompletion = new(true),
                    Result = new(childOutput)
                },
                new WriteLine(context => $"Child finished executing and said: {childOutput.Get(context)!["ChildMessage"]}")
            }
        };
    }
}
```

{% endcode %}

{% code title="ChildWorkflow\.cs" %}

```csharp
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Management.Activities.SetOutput;

namespace Elsa.Samples.AspNet.ChildWorkflows.Workflows;

public class ChildWorkflow : WorkflowBase
{
    protected override void Build(IWorkflowBuilder builder)
    {
        builder.Root = new Sequence
        {
            Activities =
            {
                new WriteLine(context => $"Input from parent: \"{context.GetInput<string>("ParentMessage")}\"."),
                new SetOutput
                {
                    OutputName = new("ChildMessage"),
                    OutputValue = new("Hello from child!")
                }
            }
        };
    }
}
```

{% endcode %}

## Using Elsa Studio

The following recorded guides demonstrate how to create a child workflow and a parent workflow that then dispatches the child workflow for execution.

{% embed url="<https://dubble.so/guides/dispatch-workflow-activity-xxljimpuovpqqlmuyrg6>" %}


# Bulk Dispatch Workflows Activity

Use `BulkDispatchWorkflows` when one parent workflow should fan out work to many child workflow instances in one step.

In `release/3.8.0`, the activity lives in `Elsa.Workflows.Runtime.Activities.BulkDispatchWorkflows` and is exposed in the `Composition` category in Elsa Studio.

## When to use it

Use `BulkDispatchWorkflows` when you need to:

* dispatch the same child workflow for each item in a collection
* optionally wait for all child workflows to finish
* react differently when individual child workflows finish or fault
* assign per-item correlation IDs

Choose the surrounding pattern based on where the fan-out should happen:

| Use this                                                                           | When                                                                                                |
| ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| [Dispatch Workflow Activity](/guides/running-workflows/dispatch-workflow-activity) | You only need one child workflow instance.                                                          |
| `BulkDispatchWorkflows`                                                            | You need one child workflow instance per item in a collection.                                      |
| `ForEach`                                                                          | You want to iterate inside the same workflow instance instead of creating child workflow instances. |
| `POST /workflow-definitions/{definitionId}/bulk-dispatch`                          | An external client needs to queue multiple instances of the same workflow definition.               |

## What it does

At execution time, Elsa:

1. evaluates `Items` into a materialized list
2. resolves the published child workflow definition from `WorkflowDefinitionId`
3. creates a new child workflow instance per item
4. sets `ParentWorkflowInstanceId` on each dispatch request so the runtime can track the parent-child relationship
5. adds `ParentInstanceId` to the child input and dispatch properties
6. merges the current item into the child workflow input
7. dispatches the child workflow through the selected channel
8. either completes immediately or waits for child completion, depending on `WaitForCompletion`

If the target workflow definition does not have a published version, the activity faults.

If `Items` is empty, the activity completes immediately even when `WaitForCompletion` is `true`.

## Input mapping

Each dispatched child workflow starts with the optional `Input` dictionary and then receives per-item input:

* if an item is a plain value, Elsa sends it under `DefaultItemInputKey` (default: `Item`)
* if an item is already an `IDictionary<string, object>`, Elsa merges that dictionary directly into the child input instead

The activity also adds `ParentInstanceId` to the child workflow input before merging item dictionaries. If your item dictionaries use the same key, they overwrite that input value. The same overwrite behavior applies to any matching keys that were already present in the optional `Input` dictionary.

## Waiting vs fire-and-forget

`WaitForCompletion` defaults to `true`.

| Setting | Runtime behavior                                                         |
| ------- | ------------------------------------------------------------------------ |
| `true`  | Creates a bookmark and waits until all dispatched child workflows finish |
| `false` | Dispatches child workflows and completes the parent activity immediately |

When Elsa waits for completion, it tracks how many child instances were dispatched and resumes the parent activity whenever a finished child workflow reports back through `ResumeBulkDispatchWorkflowActivity`.

## Outcomes and child ports

`BulkDispatchWorkflows` exposes these flowchart outcomes in its activity metadata:

* `Done`
* `Completed`
* `Canceled`

In `release/3.8.0`, the implementation completes with:

* `Done` immediately when `WaitForCompletion` is `false`
* `Done` immediately when `Items` is empty
* `Completed` and `Done` after the last child finishes when `WaitForCompletion` is `true`

You can also attach per-child ports:

* `ChildCompleted` runs for each child workflow whose sub-status is `Finished`
* `ChildFaulted` runs for each child workflow whose sub-status is `Faulted`

While those child-port activities run, Elsa adds a `ChildInstanceId` workflow variable and passes these values as workflow input:

* `WorkflowOutput`
* `WorkflowInstanceId`
* `WorkflowStatus`
* `WorkflowSubStatus`

If `WaitForCompletion` is `false`, Elsa never schedules these ports because the parent workflow does not wait for child completion events.

## Using code

### Wait for all child workflows

This example dispatches one child workflow per employee and waits for all of them to complete.

{% code title="GreetEmployeesWorkflow\.cs" %}

```csharp
using System.Collections.Generic;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Runtime.Activities;

public class GreetEmployeesWorkflow : WorkflowBase
{
    public const string DefinitionId = "greet-employees";

    protected override void Build(IWorkflowBuilder builder)
    {
        builder.WithDefinitionId(DefinitionId);

        var employees = new[]
        {
            new Dictionary<string, object> { ["Employee"] = "Alice" },
            new Dictionary<string, object> { ["Employee"] = "Bob" },
            new Dictionary<string, object> { ["Employee"] = "Charlie" }
        };

        builder.Root = new Sequence
        {
            Activities =
            {
                new BulkDispatchWorkflows
                {
                    WorkflowDefinitionId = new(EmployeeGreetingWorkflow.DefinitionId),
                    Items = new(employees),
                    WaitForCompletion = new(true)
                },
                new WriteLine("All employee greeting workflows finished.")
            }
        };
    }
}
```

{% endcode %}

{% code title="EmployeeGreetingWorkflow\.cs" %}

```csharp
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Activities;

public class EmployeeGreetingWorkflow : WorkflowBase
{
    public const string DefinitionId = "employee-greeting";

    protected override void Build(IWorkflowBuilder builder)
    {
        builder.WithDefinitionId(DefinitionId);
        var employee = builder.WithInput<string>("Employee");

        builder.Root = new WriteLine(context => $"Hello {context.GetInput<string>(employee)}");
    }
}
```

{% endcode %}

Because each item is a dictionary, the child workflow receives `Employee` directly instead of an `Item` wrapper key.

### Fire-and-forget batch dispatch

Set `WaitForCompletion` to `false` when the parent workflow should continue without waiting for the children:

```csharp
new BulkDispatchWorkflows
{
    WorkflowDefinitionId = new("SlowBulkChildWorkflow"),
    Items = new(new object[] { "A", "B", "C" }),
    WaitForCompletion = new(false)
}
```

This still records `ParentWorkflowInstanceId` on the dispatched workflow request, but the parent activity does not create a waiting bookmark.

### Per-item correlation IDs

`CorrelationIdFunction` is evaluated once per item. The current item is exposed to the expression evaluator arguments used by the activity.

```csharp
using Elsa.Expressions.JavaScript.Models;

new BulkDispatchWorkflows
{
    WorkflowDefinitionId = new("BulkChildWorkflow"),
    Items = new(new object[] { 1, 2, 3 }),
    CorrelationIdFunction = new(JavaScriptExpression.Create("`correlation-${getItem()}`")),
    WaitForCompletion = new(true)
}
```

## Elsa Studio notes

In Elsa Studio, look for `Bulk Dispatch Workflows` under the `Composition` activity category.

The main properties to configure are:

* `Workflow Definition`
* `Items`
* `Default Item Input Key`
* `Correlation ID Function`
* `Input`
* `Wait For Completion`
* `Channel`
* `Start New Trace`

Leaving `Channel` empty uses the default dispatcher channel.

Use `ChildCompleted` and `ChildFaulted` ports when the parent workflow needs per-child follow-up logic.

## Activity vs REST bulk dispatch

Elsa Server also exposes `POST /workflow-definitions/{definitionId}/bulk-dispatch`.

That endpoint is useful when an external caller wants to start the same workflow `Count` times with the same input payload.

`BulkDispatchWorkflows` is different:

* it runs inside a parent workflow
* it can send different input per item
* it can wait for children and react to child completion or faults
* it can dispatch to a configured workflow channel

## Related guides

* [Dispatch Workflow Activity](/guides/running-workflows/dispatch-workflow-activity)
* [Running Workflows](/guides/running-workflows)
* [Timer and Scheduled Workflows](/guides/running-workflows/timer-and-scheduled-workflows)


# Studio User Guide

A comprehensive guide to using Elsa Studio, the visual designer and admin UI for Elsa Workflows v3.

Elsa Studio is the visual designer and administrative interface for Elsa Workflows v3. It provides a web-based environment where you can create, edit, and manage workflows visually, monitor workflow executions, and configure your workflow automation system.

## What is Elsa Studio?

Elsa Studio is a Blazor-based web application that connects to an Elsa Server as its backend. It serves as your primary tool for:

* **Visual Workflow Design**: Create and edit workflows using an intuitive drag-and-drop interface
* **Workflow Management**: Organize, version, and publish workflow definitions
* **Instance Monitoring**: Track workflow executions, view their status, and inspect variables
* **Administration**: Manage workflow configurations and settings

Whether you're building simple automation tasks or complex business processes, Elsa Studio provides the tools you need to design and manage your workflows efficiently.

## Core Concepts for Studio Users

Before diving into the Studio interface, it's helpful to understand these key concepts:

### Workflows

A **workflow** is a sequence of activities that represents a business process or automation task. In Studio, you create workflows by placing activities on a canvas and connecting them to define the execution flow.

### Activities

**Activities** are the building blocks of workflows. Each activity represents a single unit of work, such as:

* Writing to a log
* Sending an HTTP request
* Making a decision based on conditions
* Setting or reading variables
* Triggering events

Activities have properties that you configure in the property panel, and they can produce outputs that other activities can use.

### Variables

**Variables** allow you to store and retrieve data within a workflow. You can:

* Define variables at the workflow level
* Set variable values using activities like `SetVariable`
* Access variable values in expressions throughout your workflow
* Pass data between activities using variables

Variables are essential for building dynamic workflows that respond to data and conditions.

### Inputs and Outputs

**Inputs** are data that workflows and activities receive:

* **Workflow inputs**: Data passed to the workflow when it starts
* **Activity inputs**: Properties you configure on each activity

**Outputs** are data that activities produce:

* Activities can have named outputs that subsequent activities can reference
* The last executed activity's result is available as `LastResult`
* Outputs can be used in expressions to make decisions or pass data forward

### Expressions

**Expressions** allow you to write dynamic values for activity properties. Instead of hardcoding values, you can use expressions to:

* Reference workflow variables
* Access activity outputs
* Perform calculations
* Make decisions based on data

Studio supports multiple expression types including JavaScript, C#, Liquid, and more. See the [Expressions guide](/guides/studio/expressions) for detailed information.

## Studio Interface Overview

When you open Elsa Studio, you'll see several key areas:

### Sidebar Navigation

The left sidebar provides access to the main sections of Studio:

* **Workflows**: View and manage all workflow definitions
* **Workflow Instances**: Monitor running and completed workflow executions
* **Settings**: Configure Studio preferences (availability depends on your deployment)

### Workflow List

When you click "Workflows" in the sidebar, you'll see a list of all workflow definitions. From here you can:

* Create new workflows
* Edit existing workflows
* Publish or unpublish workflows
* Delete workflows
* View workflow versions

### Designer Canvas

The workflow designer is where you build your workflows:

* **Activity Toolbox**: Browse and search available activities (usually on the left)
* **Canvas**: The main area where you drag activities and connect them
* **Connections**: Visual lines showing the flow between activities
* **Zoom Controls**: Zoom in/out and fit the workflow to the screen

### Activity Inspector / Property Panel

When you select an activity on the canvas, the property panel (usually on the right) displays:

* **Activity Name**: Give your activity a descriptive name
* **Properties**: Configure the activity's input properties
* **Expression Type Selector**: Choose how to provide values (Literal, JavaScript, C#, etc.)
* **Output Settings**: Configure which outputs to capture as variables

This is where you'll spend much of your time configuring activities and writing expressions.

## Getting Started with Studio

To start working with Elsa Studio:

1. **Access Studio**: Navigate to your Elsa Studio URL (e.g., `https://localhost:6001`)
2. **Login**: Use your credentials (default: username `admin`, password `password`)
3. **Create a Workflow**: Click "Workflows" in the sidebar, then "Create Workflow"
4. **Add Activities**: Drag activities from the toolbox onto the canvas
5. **Configure Activities**: Click an activity to open its properties in the inspector panel
6. **Connect Activities**: Drag from an activity's outcome port to another activity
7. **Test Your Workflow**: Save and run your workflow to see it in action

## Further Reading

Explore these guides to learn more about using Elsa Studio effectively:

* [**Expressions**](/guides/studio/expressions): Learn how to use JavaScript and C# expressions to reference variables and create dynamic workflows
* [**Customization**](/guides/studio/customization): Understand the main Studio customization seams in release 3.8.0, including host composition, menus, widgets, branding, and editor extensibility
* [**Custom Activity Icons**](/guides/studio/custom-icons): Add Studio-side icons for custom activities
* [**Custom UI Components**](/guides/studio/custom-ui-components): Create custom property editors for activity inputs
* [**Integration**](/guides/studio/integration): Integrate Elsa Studio into React, Angular, Blazor, or MVC applications
* [**Weaver and AI Workflow Assistance**](/guides/ai-workflow-assistance): Configure grounded AI assistance, review proposal boundaries, and troubleshoot the server/Studio connection
* [**Studio Tour & Troubleshooting**](/studio/studio-tour-troubleshooting): Detailed walkthrough of the Studio interface with troubleshooting tips
* [**Workflow Editor**](/studio/workflow-editor): Advanced features of the workflow editor
* [**Running Workflows**](/guides/running-workflows/using-elsa-studio): How to execute and test your workflows

## Tips for Success

{% hint style="info" %}
**Naming Conventions**: Give your activities descriptive names. This makes it easier to reference their outputs and understand your workflow at a glance.
{% endhint %}

{% hint style="info" %}
**Start Simple**: Begin with simple workflows to learn the basics, then gradually add complexity as you become more comfortable with the tools.
{% endhint %}

{% hint style="info" %}
**Use Variables**: Variables are your friends! They make workflows more readable and maintainable by giving names to important values.
{% endhint %}

{% hint style="warning" %}
**Expression Types Matter**: When configuring activity properties, make sure you select the correct expression type (Literal, JavaScript, C#, etc.) for your use case. Using the wrong type is a common source of errors.
{% endhint %}

## Next Steps

Ready to dive deeper? Here are some recommended paths:

* [**Expressions guide**](/guides/studio/expressions) - Learn how to work with variables and create dynamic workflows using JavaScript and C# expressions
* [**Customization**](/guides/studio/customization) - Learn how to change Studio composition, branding, widgets, and editor behavior without forking the shell
* [**Custom UI Components**](/guides/studio/custom-ui-components) - Learn how to create custom property editors for specialized activity inputs
* [**Integration guide**](/guides/studio/integration) - Discover how to integrate Elsa Studio into your existing React, Angular, Blazor, or MVC application


# Expressions

Learn how Elsa Studio expression editors map to Elsa Server expression engines, how to access workflow variables, and which syntax choices are actually available in Elsa 3.8.

Elsa Studio lets you enter activity values in different expression syntaxes. The exact choices you see are not hardcoded in Studio alone: Studio asks Elsa Server for the available expression descriptors and renders those options in the property editor.

This matters for two reasons:

* The list of available expression types depends on which expression features your server enables.
* Some syntaxes shown in Studio are UI helpers, while others are real runtime expression engines.

## How expression selection works

For most activity inputs, Studio shows a **Default** option plus one or more code-oriented expression types.

* **Default** means: use the activity's normal UI editor for that field.
* **Literal** and **Object** are built-in Studio/UI syntaxes used behind the scenes for plain values and structured values.
* **JavaScript**, **Liquid**, **C#**, **Python**, and other custom types are provided by the server.

In the 3.8 sample server, the app enables these expression engines:

```csharp
.UseCSharp(...)
.UseJavaScript(...)
.UsePython(...)
.UseLiquid(...)
```

If your server does not register one of these features, Studio will not offer that syntax.

{% hint style="info" %}
In Elsa 3.8, C# and Python are only browsable in Studio when host code execution is allowed for those engines. If you do not see them in the picker, check your server configuration first.
{% endhint %}

## Expression matrix

The following matrix separates what Studio shows from what the backend actually evaluates.

| Studio choice                 | Source                      | Authoring style             | Notes                                                                                                   |
| ----------------------------- | --------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------- |
| `Default`                     | Studio UI mode              | Field-specific editor       | Not a separate runtime expression engine.                                                               |
| `Variable`                    | Server descriptor           | Picker UI                   | Resolves to a selected workflow variable. Different from `variables` in JavaScript or `Variable` in C#. |
| `Input`                       | Server descriptor           | Picker UI                   | Resolves to a selected workflow input. Different from `getInput(...)` or C# `Input`.                    |
| `JavaScript`                  | `UseJavaScript(...)`        | Monaco code editor          | Best general-purpose dynamic option in Studio.                                                          |
| `Liquid`                      | `UseLiquid(...)`            | Monaco code editor          | Best for text templates.                                                                                |
| `C#`                          | `UseCSharp(...)`            | Monaco code editor          | Browsable only when host code execution is allowed.                                                     |
| `Python`                      | `UsePython(...)`            | Monaco code editor          | Browsable only when host code execution is allowed.                                                     |
| Custom types such as `Secret` | Feature-specific descriptor | Usually picker or custom UI | Only appears when the related backend feature is installed.                                             |

Studio also has hidden internal descriptors such as `Literal` and `Object` that support plain values and structured values behind the scenes.

## Which syntax should you use?

Use the simplest option that matches the job:

* **Default** for plain text, numbers, booleans, dropdowns, and other normal field editors.
* **Variable** when you want the field to resolve directly from a selected workflow variable without writing code.
* **Input** when you want the field to resolve from a selected workflow input without writing code.
* **JavaScript** for most dynamic value composition and workflow-variable lookups.
* **Liquid** for templated text output.
* **C#** when you explicitly want Roslyn-based expressions and have enabled them for trusted authors.
* **Python** only when your server is configured for Python.NET expressions and you need it specifically.

## Picker syntaxes versus code-expression objects

This is the most common point of confusion in Studio:

* **Variable** and **Input** in the syntax picker are expression types backed by picker UI.
* `variables` in JavaScript is a runtime object injected into the JavaScript engine.
* `Variable` and `Input` in C# are runtime proxies injected into the C# evaluator.

So these are related, but they are not the same thing.

### Variable picker

Use **Variable** when a property should simply read from one workflow variable and you do not need any extra logic.

Example: choose the `CustomerName` variable from the picker for a text field.

### Input picker

Use **Input** when a property should read directly from one declared workflow input.

Example: choose the `OrderId` workflow input from the picker for a downstream activity field.

## JavaScript expressions

JavaScript is usually the most convenient dynamic syntax in Studio.

### Reading variables

If variable wrappers are enabled and your variable names are valid JavaScript property names, you can use:

```javascript
variables.OrderId
variables.Customer.Name
variables.Items[0]
```

Elsa also exposes explicit helper functions:

```javascript
getVariable("OrderId")
getVariable("Customer")
```

Use `getVariable` when:

* the variable name is not a valid JavaScript identifier.
* variable wrappers were disabled on the server.
* you want to avoid depending on the `variables.SomeName` convenience wrapper.

### Writing variables

To assign a value from JavaScript, use:

```javascript
setVariable("OrderId", getGuidString())
setVariable("Status", "Approved")
```

### Accessing inputs and outputs

Elsa's JavaScript helpers also include:

```javascript
getInput("CustomerId")
getOutputFrom("SendHttpRequest", "ParsedContent")
getLastResult()
```

### Practical JavaScript examples

```javascript
variables.Total > 1000 ? "Priority" : "Standard"
```

```javascript
`${variables.FirstName} ${variables.LastName}`
```

```javascript
getOutputFrom("SendHttpRequest", "ParsedContent")
```

```javascript
setVariable("CorrelationCopy", getCorrelationId())
```

### Important wrapper limitation

The `variables.SomeName` style depends on two JavaScript runtime settings:

* variable wrappers must be enabled.
* variable copying must be enabled.

If either is disabled, use `getVariable(...)` and `setVariable(...)` instead.

## C# expressions

C# expressions are available when the server enables the C# expression feature and allows host code execution for trusted workflow authors.

Elsa generates a variable proxy for C# expressions. In Elsa 3.8, both `Variables` and `Variable` work, but `Variable` is the preferred alias.

### Reading variables

```csharp
Variable.OrderId
Variable.Get<Guid>("OrderId")
Variable.Get<string>("CustomerName")
```

### Writing variables

```csharp
Variable.Set("OrderId", Guid.NewGuid());
Variable.Set("Status", "Completed");
```

### Accessing inputs and outputs

```csharp
Input.CustomerId
Input.Get<Guid>("CustomerId")
Output.From<string>("SendHttpRequest", "ParsedContent")
Output.LastResult
```

### Accessing workflow metadata

```csharp
WorkflowInstanceId
CorrelationId
WorkflowInstanceName
```

You can also assign metadata in C# expressions when the scenario supports it:

```csharp
CorrelationId = Variable.Get<string>("OrderId");
WorkflowInstanceName = $"Order {Variable.Get<string>("OrderId")}";
```

### Practical C# examples

```csharp
Variable.TotalAmount > 1000 ? "Priority" : "Standard"
```

```csharp
$"{Variable.FirstName} {Variable.LastName}"
```

```csharp
Output.From<string>("SendHttpRequest", "ParsedContent")
```

### When to prefer `Get<T>()`

Use `Get<T>()` when:

* the variable name is not a valid generated property name.
* you want explicit typing.
* wrappers were disabled in C# options.

## Liquid expressions

Liquid is best for text templating.

Elsa 3.8 registers workflow variables through the `Variables` object and workflow or activity inputs through `Input`.

```liquid
Hello {{ Variables.CustomerName }}
Order {{ Variables.OrderId }} is {{ Variables.Status }}
Customer input: {{ Input.CustomerId }}
```

Liquid also exposes workflow metadata:

```liquid
Instance: {{ WorkflowInstanceId }}
Correlation: {{ CorrelationId }}
Definition: {{ WorkflowDefinitionId }}
Version: {{ WorkflowDefinitionVersion }}
```

### Practical Liquid examples

```liquid
Hello {{ Variables.FirstName }} {{ Variables.LastName }}
```

```liquid
Order {{ Variables.OrderId }} total is {{ Variables.TotalAmount }}
```

Use Liquid when the result should primarily be formatted text, not when you need more procedural logic.

## Python expressions

Python expressions are available only when the Python feature is enabled and host code execution is allowed.

Elsa injects these globals into the Python scope:

* `execution_context`
* `input`
* `output`
* `outcome`
* `variables`

Typical variable access looks like this:

```python
variables.OrderId
variables.set("Status", "Processed")
variables.get("CustomerName")
```

### Accessing inputs and outputs

```python
input.Get("CustomerId")
output.Get("SendHttpRequest", "ParsedContent")
output.LastResult
```

### Practical Python examples

```python
variables.get("TotalAmount") > 1000
```

```python
variables.set("ResponseBody", output.Get("SendHttpRequest", "ParsedContent"))
variables.get("ResponseBody")
```

{% hint style="info" %}
Python expressions are executed through Python.NET in Elsa 3.8. Keep them for intentionally enabled, trusted-author scenarios rather than as the default authoring path in Studio.
{% endhint %}

## Feature-specific custom expression types

Studio can also surface feature-specific expression types beyond the core set above.

For example, Elsa's secrets feature contributes a `Secret` expression descriptor with its own custom UI instead of a code editor. See [Secrets management](/guides/security/secrets-management) for the store choices, lifecycle, permissions, and runtime reference contract. The general rule is:

* if a backend feature registers an expression descriptor and marks it browsable, Studio can show it.
* if the descriptor provides a custom `UIHint`, Studio renders that custom picker/editor instead of Monaco.

That is why two Elsa deployments can show different syntax choices for the same activity field.

## Variable naming guidance

If you want the most convenient cross-language experience in Studio, use variable names that are valid identifiers, such as:

* `OrderId`
* `CustomerName`
* `RetryCount`

Avoid names that require quoting or special handling, such as:

* `order-id`
* `customer name`
* `123value`

Those names can still work, but you will need accessor methods like `getVariable("order-id")` instead of wrapper-style property access.

## Variable storage and expressions

Expressions read the current value of workflow variables regardless of whether the variable is stored in memory or in workflow-instance storage.

In Elsa 3.8:

* **Memory** storage keeps the value in memory for the lifetime of the current execution context.
* **Workflow Instance** storage persists the value in workflow state.
* The legacy **Workflow** storage driver still exists for backward compatibility, but `Workflow Instance` is the current persisted option.

Choose storage based on lifecycle and suspension/resumption needs; choose expression syntax based on authoring convenience.

## Recommended patterns

* Use **Default** unless you actually need dynamic behavior.
* Use **JavaScript** for most dynamic field values in Studio.
* Use **Liquid** for generated messages, templates, and text bodies.
* Use **C#** and **Python** only for trusted-author scenarios where you intentionally enable host code execution.
* Prefer identifier-friendly variable names so wrapper syntax stays simple.

## Common pitfalls

### "I don't see C# or Python in Studio"

Usually this means the backend did not enable that engine, or host code execution for that engine is disabled.

### "`variables.MyValue` does not work in JavaScript"

Check these first:

* the variable name is a valid identifier.
* JavaScript variable wrappers were not disabled.
* JavaScript variable copying was not disabled.

If any of those are not true, switch to:

```javascript
getVariable("MyValue")
```

### "My field shows Default instead of a code editor"

That is normal. `Default` uses the activity's UI hint. Switch the syntax picker to a code-backed expression type such as JavaScript or Liquid if you want to write an expression.

## See also

* [Workflow Instance Variables](/operate/workflow-instance-variables)
* [Using Elsa Studio](/guides/running-workflows/using-elsa-studio)
* [JavaScript IntelliSense type definitions](/guides/studio/javascript-type-definition-providers)


# JavaScript IntelliSense Type Definitions

Extend Elsa Studio JavaScript IntelliSense with release-backed TypeScript declarations from the Elsa Server.

Use a JavaScript type-definition provider when custom workflow values should be discoverable in Elsa Studio's JavaScript editor. The provider supplies TypeScript declarations for Monaco IntelliSense; it does not add a runtime object, function, variable, or CLR type to the JavaScript engine.

This guide is based on the `release/3.8.0` source code in `elsa-core` and `elsa-studio`.

## How the pieces fit together

When a Studio user opens a JavaScript expression editor for an activity input:

1. Studio sends the workflow definition ID, activity type name, and property name to `POST /scripting/javascript/type-definitions/{workflowDefinitionId}`.
2. Elsa Server resolves the latest workflow graph and asks every registered JavaScript definition provider for declarations.
3. Core renders the combined result as a TypeScript declaration document and returns it as `application/x-typescript`.
4. Studio adds the document to Monaco with `javascriptDefaults.addExtraLib`, enabling completion and type information for the open JavaScript editor.

The declaration document is contextual: a provider receives the workflow graph, the activity type name, the property name, and a cancellation token. A provider can therefore emit declarations for a particular activity input or workflow shape, rather than publishing one unconditional global library.

## Choose the right extension point

The JavaScript feature has separate provider contracts:

| Goal                                   | Extension point                 |
| -------------------------------------- | ------------------------------- |
| Declare a custom type for IntelliSense | `ITypeDefinitionProvider`       |
| Describe a global function             | `IFunctionDefinitionProvider`   |
| Describe a global variable             | `IVariableDefinitionProvider`   |
| Make a .NET type available at runtime  | `JintOptions.RegisterType(...)` |

The first three affect the generated declaration file. `RegisterType` affects Jint runtime configuration and is a separate security-sensitive decision. A type-definition provider does not make `new Order()` or `order.Total` execute successfully by itself; the expression still needs a real runtime value or function supplied by the workflow context or JavaScript configuration.

## Add a provider

Reference the JavaScript expressions package and derive from `TypeDefinitionProvider`:

```csharp
using Elsa.Expressions.JavaScript.TypeDefinitions.Abstractions;
using Elsa.Expressions.JavaScript.TypeDefinitions.Models;

public sealed class OrderIntellisenseProvider : TypeDefinitionProvider
{
    protected override IEnumerable<TypeDefinition> GetTypeDefinitions(
        TypeDefinitionContext context)
    {
        if (context.ActivityTypeName != "Acme.SendOrder")
            yield break;

        yield return new TypeDefinition
        {
            DeclarationKeyword = "interface",
            Name = "Order",
            Properties =
            {
                new PropertyDefinition { Name = "Id", Type = "string" },
                new PropertyDefinition { Name = "Total", Type = "number" },
                new PropertyDefinition { Name = "IsPriority", Type = "boolean" }
            }
        };
    }
}
```

`DeclarationKeyword` is rendered directly into the TypeScript declaration, so use a keyword that produces valid output with the release renderer, such as `interface`, `class`, or `enum`. Property `Type` values are TypeScript type expressions such as `string`, `number`, `boolean`, or an already-declared type. The provider should emit valid, stable names because the generated text is inserted directly into Monaco.

The provider can also override the asynchronous method when declarations need to be loaded from a service. Honor `context.CancellationToken` for remote or expensive work.

## Register the provider

Register the provider in the server's dependency-injection container alongside the JavaScript feature:

```csharp
using Elsa.Extensions;
using Elsa.Expressions.JavaScript.Extensions;

services.AddElsa(elsa =>
{
    elsa.UseJavaScript();
});

services.AddTypeDefinitionProvider<OrderIntellisenseProvider>();
```

Core registers the provider as a scoped service and includes all registered `ITypeDefinitionProvider` instances when it builds the declaration document. Constructor injection is therefore available for providers that need access to application services.

## Keep runtime behavior separate

This is the boundary to keep in mind:

```csharp
// Runtime configuration: changes what Jint can access.
elsa.UseJavaScript(options => options.RegisterType<Order>());

// Editor configuration: changes what Studio can suggest and type-check.
services.AddTypeDefinitionProvider<OrderIntellisenseProvider>();
```

The two registrations can be used together, but neither one replaces the other. If a workflow variable is the runtime value, create or bind that variable through the normal workflow APIs. If its shape should appear in Studio, emit a matching declaration. If a custom global function is available at runtime, describe it with an `IFunctionDefinitionProvider` as well.

Do not enable broad CLR access only to make IntelliSense understand a type. `AllowClrAccess` is intended for trusted scenarios; editor declarations are the safer way to describe a known shape to workflow authors.

## Verify the Studio path

After registering the provider:

1. Enable JavaScript expressions on the Elsa Server.
2. Ensure the connected Studio host can read the workflow definition.
3. Open an activity input whose expression syntax is **JavaScript**.
4. Type the declared type or property name and confirm that Monaco offers the expected completion or type information.

The endpoint requires either `read:*` or the specific `read:javascript-type-definitions` permission. A missing permission can look like a provider problem because Studio cannot load the declaration library. The endpoint resolves the latest workflow graph; a missing workflow definition returns an API error instead of a declaration document.

## Troubleshooting

### The type does not appear in completion

Check the following in order:

* The server has `UseJavaScript()` enabled.
* The provider is registered in the server process connected to Studio.
* The provider's context filters do not exclude the current activity type.
* The declaration uses valid TypeScript syntax and identifiers.
* The Studio client has `read:javascript-type-definitions` or a wildcard permission.
* The input is using the **JavaScript** expression type, not **Default**, **Liquid**, or a custom UI editor.

### The editor suggests the type but the workflow fails

That is expected when only a declaration was registered. IntelliSense does not create runtime values. Bind the value through workflow inputs, variables, or activity outputs, or configure the corresponding JavaScript runtime behavior.

### The declaration is stale

Studio requests the declaration document when the JavaScript Monaco editor is initialized. Reopen or reinitialize the editor after changing provider output, and ensure the server is running the updated provider registration.

## Release source references

This page was checked against `release/3.8.0`:

* Core [`ITypeDefinitionProvider`](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Contracts/ITypeDefinitionProvider.cs)
* Core [`TypeDefinitionContext`](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Models/TypeDefinitionContext.cs)
* Core [`TypeDefinitionService`](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/src/modules/Elsa.Expressions.JavaScript/TypeDefinitions/Services/TypeDefinitionService.cs)
* Core [`AddTypeDefinitionProvider`](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/src/modules/Elsa.Expressions.JavaScript/Extensions/DependencyInjectionExtensions.cs)
* Core [type-definition endpoint](https://github.com/elsa-workflows/elsa-core/blob/release/3.8.0/src/modules/Elsa.Expressions.JavaScript/Endpoints/TypeDefinitions/Endpoint.cs)
* Studio [`TypeDefinitionService`](https://github.com/elsa-workflows/elsa-studio/blob/release/3.8.0/src/framework/Elsa.Studio.Shared/Services/TypeDefinitionService.cs)
* Studio [`JavaScriptMonacoHandler`](https://github.com/elsa-workflows/elsa-studio/blob/release/3.8.0/src/framework/Elsa.Studio.Shared/Monaco/Handlers/JavaScriptMonacoHandler.cs)


# Custom Activity Icons

Add custom activity icons to Elsa Studio with a release-backed display settings provider.

Use a custom activity icon when your Elsa Studio users need to recognize a domain activity quickly in the activity picker, workflow designer, or runtime inspection views. In Elsa `release/3.8.0`, icons are a Studio display concern: the Core `ActivityDescriptor` contains activity metadata, but no icon field.

That means the icon provider belongs in the Elsa Studio host (Server, WASM, or Custom Elements), not in the Elsa Server runtime. It changes how Studio renders an activity; it does not add the activity to the server, change its execution behavior, or install an icon asset for other clients.

## How the icon path works

The release implementation has three parts:

1. `IActivityDisplaySettingsProvider.GetSettings()` returns a dictionary keyed by the exact activity `TypeName`.
2. `ActivityDisplaySettings` supplies an `Icon` string and a `Color` string.
3. `DefaultActivityDisplaySettingsRegistry` combines all providers and returns the matching settings. Unknown activity types receive Studio's default icon and color.

The registry is used by the activity pickers and by designer and workflow instance components. A custom provider therefore updates several Studio surfaces at once without changing workflow JSON.

## Add a provider

Create a Studio-side provider in the project or module that owns your Studio customization:

```csharp
using System.Collections.Generic;
using Elsa.Studio.Workflows.UI.Contracts;
using Elsa.Studio.Workflows.UI.Models;

public sealed class AcmeActivityDisplaySettingsProvider
    : IActivityDisplaySettingsProvider
{
    private const string InvoiceIcon = """
        <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
            <path fill="currentColor" d="M4 3h16v18H4z" />
            <path fill="currentColor" d="M7 7h10v2H7zm0 4h10v2H7zm0 4h6v2H7z" />
        </svg>
        """;

    public IDictionary<string, ActivityDisplaySettings> GetSettings() =>
        new Dictionary<string, ActivityDisplaySettings>
        {
            ["Acme.SendInvoice"] = new("#2563eb", InvoiceIcon)
        };
}
```

The key, `Acme.SendInvoice`, must match the activity descriptor's `TypeName` exactly. It is not the display name shown to designers. For a CLR activity, confirm the value in the descriptors response or in the activity descriptor that Studio receives from the server.

The `Icon` value is passed to the Studio components as an icon string. Elsa's stock provider uses both `ElsaStudioIcons` and MudBlazor icon constants. An inline SVG is useful when you own the artwork, but keep it small and suitable for the icon slot. If the SVG should follow the configured activity color, use `currentColor` for its `fill` or `stroke`; the tree picker applies the activity color to those exact SVG attributes.

## Register it in the Studio host

Register the provider after the workflows module in the Studio host's dependency-injection setup. The same pattern applies to Server, WASM, and Custom Elements hosts; use the container exposed by that host.

```csharp
builder.Services.AddWorkflowsModule();
builder.Services.AddActivityDisplaySettingsProvider<
    AcmeActivityDisplaySettingsProvider>();
```

For a WASM host, the equivalent is `services.AddWorkflowsModule()` followed by `services.AddActivityDisplaySettingsProvider<...>()`. The generic registration adds the provider as a scoped `IActivityDisplaySettingsProvider`.

Do not register this provider only in `elsa-core` or only on the Elsa Server. Those processes do not own the Studio display registry. If you deploy more than one Studio host, include the provider in each host that should show the custom icon.

## Dynamic activity sets

The provider may build its dictionary from the Studio activity registry when a family of activities is discovered dynamically. The released Agents extension uses this pattern: it selects descriptors whose `RootType` custom property is `AgentActivity`, then maps each descriptor's `TypeName` to one shared robot icon and color.

Use a dynamic provider only when the set of activity type names really is dynamic. For a fixed set of custom activities, a static dictionary is easier to review and less sensitive to registry timing.

## Provider precedence and fallback

The release registry evaluates providers in sequence and assigns each returned dictionary entry into one combined dictionary. If two providers return the same type name, the later assignment wins. Use unique keys or make an override intentional; do not depend on incidental registration order.

If no provider returns a matching type name, Studio uses its built-in default icon and color. This fallback is useful while a provider is being rolled out, but it can also hide a type-name mismatch. When an icon does not appear, check the descriptor `TypeName` first.

The registry builds its combined dictionary lazily and caches it for the current scope. `IActivityDisplaySettingsRegistry` exposes `MarkStale()` to clear that cache, but the release source does not automatically invalidate it when an arbitrary provider's backing data changes. Prefer stable startup mappings; if your provider is genuinely dynamic, make cache invalidation part of the same explicit refresh operation.

## Troubleshooting

### The activity still has the default icon

Check these in order:

1. The provider is registered in the Studio host that the browser is actually using.
2. The dictionary key exactly matches the activity descriptor `TypeName`, including punctuation and casing.
3. The provider is included in the deployed Studio module or application.
4. The icon string is non-empty and is accepted by the Studio icon component.
5. The browser has loaded the updated Studio deployment rather than a cached application bundle.

### The icon appears in one surface but not another

The activity picker and designer use the same display-settings registry, but they render the icon through different components. Verify the SVG markup is valid and keep `fill="currentColor"` or `stroke="currentColor"` where the activity color should be applied. A library icon constant can avoid SVG markup differences between components.

### The activity is missing entirely

An icon provider does not register activities. Confirm the Elsa Server returns the activity descriptor and that the activity is browsable. Use the [activity type provider guide](/extensibility/activity-type-providers) when the activity itself is generated dynamically or is not present in the activity picker.

## Related guides

* [Customizing Elsa Studio](/guides/studio/customization)
* [Custom Activities](/extensibility/custom-activities)
* [Activity Type Providers](/extensibility/activity-type-providers)

## Release source

This page was checked against the following `release/3.8.0` implementations:

* [Studio display settings contract](https://github.com/elsa-workflows/elsa-studio/blob/a067420e196245e0f5ecd755d318fa1de16364f2/src/modules/Elsa.Studio.Workflows.Core/UI/Contracts/IActivityDisplaySettingsProvider.cs)
* [Studio display settings model](https://github.com/elsa-workflows/elsa-studio/blob/a067420e196245e0f5ecd755d318fa1de16364f2/src/modules/Elsa.Studio.Workflows.Core/UI/Models/ActivityDisplaySettings.cs)
* [Studio display settings registry contract](https://github.com/elsa-workflows/elsa-studio/blob/a067420e196245e0f5ecd755d318fa1de16364f2/src/modules/Elsa.Studio.Workflows.Core/UI/Contracts/IActivityDisplaySettingsRegistry.cs)
* [Studio display settings registry](https://github.com/elsa-workflows/elsa-studio/blob/a067420e196245e0f5ecd755d318fa1de16364f2/src/modules/Elsa.Studio.Workflows.Core/Domain/Services/DefaultActivityDisplaySettingsRegistry.cs)
* [Studio provider registration](https://github.com/elsa-workflows/elsa-studio/blob/a067420e196245e0f5ecd755d318fa1de16364f2/src/modules/Elsa.Studio.Workflows.Core/Extensions/ServiceCollectionExtensions.cs)
* [Studio tree activity picker](https://github.com/elsa-workflows/elsa-studio/blob/a067420e196245e0f5ecd755d318fa1de16364f2/src/modules/Elsa.Studio.Workflows/ActivityPickers/Treeview/ActivityPicker.razor.cs)
* [Studio icon color handling](https://github.com/elsa-workflows/elsa-studio/blob/a067420e196245e0f5ecd755d318fa1de16364f2/src/modules/Elsa.Studio.Workflows/ActivityPickers/Treeview/ActivityTreeItem.cs)
* [Core activity descriptor](https://github.com/elsa-workflows/elsa-core/blob/e59a0f172166efc24244f8fc49d8e8c33f05166b/src/modules/Elsa.Workflows.Core/Models/ActivityDescriptor.cs)
* [Extensions Agents provider example](https://github.com/elsa-workflows/elsa-extensions/blob/335a26495318f6ee1528bf2723b7333c753ce9a2/src/modules/agents/Elsa.Studio.Agents/UI/Providers/DefaultActivityDisplaySettingsProvider.cs)


# Customization

Source-backed guide to the main Elsa Studio customization seams in release 3.8.0, including host composition, branding, menus, widgets, activity pickers, and editor extensibility.

This guide is based on the `release/3.8.0` source code in `elsa-studio` and, where activity metadata is involved, `elsa-core`.

Use this guide when you are embedding or hosting Elsa Studio and need to change how Studio looks, what it exposes, or how its editors behave.

If you only need host setup, start with [Studio Integration](/guides/studio/integration). If you only need embedded surfaces, go straight to [Custom Elements Embedding](/guides/studio/integration/custom-elements).

## Start With The Smallest Useful Seam

In Elsa Studio 3.8.0, customization is mostly done through dependency injection and feature modules.

| Goal                                    | Use this seam                                          | Source-backed entry point                                                   |
| --------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------- |
| Change Studio shell services or options | Host startup                                           | `AddShell`, `ShellOptions`                                                  |
| Change branding in a standalone host    | Replace `IBrandingProvider`                            | `MainLayout`, server host `Program.cs`                                      |
| Add or remove Studio capabilities       | Host module registration                               | `AddWorkflowsModule`, `AddDashboardModule`, `AddSecretsModule`, and similar |
| Add navigation items                    | `IMenuProvider`                                        | `DefaultMenuService`                                                        |
| Add app-bar UI                          | `IAppBarService` from an `IFeature`                    | `DefaultAppBarService`                                                      |
| Add panels, tabs, or editor widgets     | `IWidget` or `IWidgetRegistry`                         | `DefaultWidgetRegistry`                                                     |
| Change the workflow activity picker     | Replace `IActivityPickerComponentProvider`             | workflows module plus host override                                         |
| Add icons for custom activities         | `IActivityDisplaySettingsProvider`                     | `AddActivityDisplaySettingsProvider<T>`                                     |
| Render a new input editor               | Studio `IUIHintHandler` plus backend `UIHint` metadata | `AddDefaultUIHintHandlers`, `ActivityDescriber`                             |
| Decorate an existing input editor       | `IUIFieldExtensionHandler`                             | `FieldExtension.razor`                                                      |

Pick the narrowest seam that solves the problem. That keeps your host close to the stock Studio behavior and reduces upgrade risk.

## Host-Level Composition

The three released Studio hosts all compose Studio by registering services and modules in `Program.cs`:

* `src/hosts/Elsa.Studio.Host.Server`
* `src/hosts/Elsa.Studio.Host.Wasm`
* `src/hosts/Elsa.Studio.Host.CustomElements`

That means host composition is the first customization seam.

### Shell Options

`AddShell` configures shared shell services and binds `ShellOptions`. In release 3.8.0, the exposed shell option is:

```json
{
  "Shell": {
    "DisableAuthorization": false
  }
}
```

`App.razor.cs` reads this option to decide whether authorization should be enforced in the shell.

### Branding

The shared services layer registers `DefaultBrandingProvider`, and `MainLayout.razor` renders the current provider inside the drawer header.

The server host shows the supported replacement pattern by registering `StudioBrandingProvider` as `IBrandingProvider`.

Use this seam when you need:

* custom logos or product naming
* organization-specific shell branding
* different login or navigation branding in a dedicated Studio host

### Module Set

Studio capabilities are opt-in at host startup. For example, the released server host registers:

* dashboard modules
* workflows and workflow dashboard modules
* alterations
* diagnostics modules
* secrets
* localization

If a host does not register a module, that capability is not present in the shell. This is the primary seam for building a smaller, purpose-built Studio.

## Navigation, App Bar, And Feature Gating

Studio uses `IFeature`, `IMenuProvider`, and `IAppBarService` to compose user-facing shell behavior.

### Menus

`DefaultMenuService` asks every registered `IMenuProvider` for menu items and then orders the combined result.

Use `IMenuProvider` when you want to:

* add a new top-level navigation item
* add menu entries for a custom Studio module
* hide stock menu areas by omitting the corresponding module from the host

### App Bar

`MainLayout.razor` renders `AppBarService.AppBarComponents`.

Features such as localization and environment selection add app-bar elements during feature initialization. Use `IAppBarService` when you need global shell controls such as:

* environment switchers
* tenant switchers
* custom status or action buttons

### Remote Feature Gating

`DefaultFeatureService` initializes all local `IFeature` registrations, but it skips any feature decorated with `RemoteFeatureAttribute` when the backend does not advertise the matching capability.

This is how modules such as OpenTelemetry and console logs stay absent from the shell when the connected Elsa Server does not support them.

Use the same pattern for optional modules that should appear only when a backend feature is available.

## Widgets And Editor Surface Extensions

Widgets are the main seam for adding UI inside existing Studio surfaces.

`DefaultWidgetRegistry` collects all registered `IWidget` instances and renders them by zone and order.

Use widgets when you need to extend an existing page rather than create a completely separate screen.

Examples from the released source include:

* workflow definition metadata, settings, and info widgets
* workflow definition labels widgets
* console log widgets in workflow instance views
* dashboard widgets and dashboard companion widgets
* platform submission widgets from `AddPlatformIntegrationModule`; see [Platform Artifact Submission](/guides/studio/integration/platform-integration) when a Studio host needs to register workflow artifacts with Elsa Platform

Use widgets for:

* extra tabs or panels in workflow or instance screens
* organization-specific metadata editors
* submit or approval actions tied to existing Studio pages

## Workflow Editor Customization

The workflow editor has three main seams in release 3.8.0.

### Activity Picker

`AddWorkflowsModule()` registers `AccordionActivityPickerComponentProvider` by default.

The server host replaces it with `TreeviewActivityPickerComponentProvider`.

If you need a different activity browsing experience, replace `IActivityPickerComponentProvider` in your host.

### Input Editors

Studio-side input editors are selected through `IUIHintHandler`.

`AddDefaultUIHintHandlers()` registers the built-in handlers for hints such as:

* `singleline`
* `dropdown`
* `checkbox`
* `json-editor`
* `code-editor`
* `workflow-definition-picker`
* `dynamic-outcomes`

If you introduce a new backend `UIHint`, register a matching Studio `IUIHintHandler`.

For the full backend-plus-Studio flow, see [Custom UI Components](/guides/studio/custom-ui-components).

### Field Decorations

`FieldExtension.razor` wraps input editors and renders matching `IUIFieldExtensionHandler` instances above or below the editor.

Use field extensions when you want to:

* add helper UI around an existing editor
* add syntax-specific controls
* add activity-specific hints or toolbars

Use this seam when the stock editor is still correct and only the framing needs to change.

For the detailed contract and example, see [Field Extensions](/studio/workflow-editor/field-extensions).

## Backend Metadata Still Matters

Some Studio customization starts in `elsa-core`, not `elsa-studio`.

`ActivityDescriber` builds input descriptors from activity metadata, and `PropertyUIHandlerResolver` combines:

* explicit `UIHandler` or `UIHandlers` from `[Input]`
* default property UI handlers associated with the selected backend UI hint

The resulting UI metadata is sent to Studio in `InputDescriptor.UISpecifications`.

If the metadata marks an input for refresh, Studio recomputes descriptor options through:

`POST /descriptors/activities/{activityTypeName}/options/{propertyName}`

That split matters:

* use `elsa-core` to define activity metadata and option-generation logic
* use `elsa-studio` to decide how that metadata is rendered

## Choosing The Right Customization Shape

Use a dedicated standalone host when you need:

* a full Studio application
* custom branding
* server-side or SPA-level auth handling
* a curated module set for operators or designers

Use custom-elements embedding when you need:

* selected Studio surfaces inside another application shell
* host-controlled navigation and auth
* incremental adoption instead of a full Studio shell

Use widgets, menu providers, and app-bar features when you need to extend the stock shell without forking its pages.

Use UI hint handlers and field extensions when the change belongs inside the workflow inspector rather than the shell.

For custom activity icons, see [Custom Activity Icons](/guides/studio/custom-icons).

## Related Guides

* [Studio Integration](/guides/studio/integration)
* [Custom Elements Embedding](/guides/studio/integration/custom-elements)
* [Custom UI Components](/guides/studio/custom-ui-components)
* [Field Extensions](/studio/workflow-editor/field-extensions)




---

[Next Page](/llms-full.txt/1)

