This documentation provides a comprehensive guide for junior developers to understand, set up, and work with the Workflow-Driven Application.
- Architecture Overview
- Project Structure
- Key Concepts
- Component Details
- Setup Guide
- Use Cases and Sequence Diagrams
- API Reference
- Troubleshooting
The application follows a Clean Architecture pattern with a multi-tenant workflow execution engine. It dynamically maps user actions (like "Add to Cart") to Elsa workflows without requiring code changes.
flowchart TB
subgraph Frontend
UI[Angular + PrimeNG]
end
subgraph BackendAPI[Backend API]
API[.NET 8 Web API]
MediatR[MediatR Handler]
TenantMW[Tenant Middleware]
end
subgraph WorkflowEngine[Workflow Engine]
Elsa[Elsa Server + Studio]
end
subgraph DataLayer[Data Layer]
PG[(PostgreSQL)]
end
subgraph Identity
Auth0[Auth0 Organizations]
end
UI -->|HTTP + JWT| API
API --> TenantMW
TenantMW --> MediatR
MediatR -->|Execute Workflow| Elsa
API -->|EF Core| PG
Elsa -->|Store Workflows| PG
UI -->|OAuth 2.0| Auth0
API -->|Validate JWT| Auth0
| Component | Technology | Port | Purpose |
|---|---|---|---|
| Frontend | Angular 18 + PrimeNG | 4200 | User interface for executing actions and admin management |
| Backend API | .NET 8, MediatR | 5000 | REST API, action routing, trigger management |
| Elsa Server | Elsa Workflows v3 | 14000 | Workflow execution engine with visual designer |
| Database | PostgreSQL 16 | 5432 | Stores triggers, tenants, workflow definitions |
| Auth Provider | Auth0 | - | Authentication with multi-tenancy via Organizations |
flowchart LR
A[User Action] --> B[API Controller]
B --> C[MediatR Handler]
C --> D{Find Trigger}
D -->|Found| E[Execute Workflow]
D -->|Not Found| F[Return Error]
E --> G[Elsa Server]
G --> H[Return Result]
H --> I[Response to User]
PDS Sample/
├── docker-compose.yml # Docker orchestration for all services
├── init-db.sql # Database initialization script
├── README.md # Quick start guide
├── DOCUMENTATION.md # This file
├── workflows/ # Sample Elsa workflow JSON files
│ ├── cart-add-item.json
│ ├── cart-remove-item.json
│ ├── order-checkout.json
│ ├── order-pay.json
│ └── README.md
└── src/
├── WorkflowApp.sln # Visual Studio solution file
│
├── WorkflowApp.Domain/ # Domain Layer (innermost)
│ ├── Entities/
│ │ ├── Tenant.cs # Tenant/organization entity
│ │ ├── WorkflowTrigger.cs # Action-to-workflow mapping
│ │ └── WorkflowExecution.cs # Execution history
│ ├── Enums/
│ │ └── TriggerSource.cs # Auto/Manual trigger source
│ └── Interfaces/
│ ├── ITenantProvider.cs # Tenant context abstraction
│ ├── ITriggerRegistry.cs # Trigger management abstraction
│ └── ITenantScoped.cs # Multi-tenancy marker interface
│
├── WorkflowApp.Contracts/ # Shared DTOs
│ ├── Actions/
│ │ ├── ExecuteEntityActionRequest.cs
│ │ └── WorkflowExecutionResult.cs
│ └── Elsa/
│ └── WorkflowModels.cs # Elsa API DTOs
│
├── WorkflowApp.Application/ # Application Layer
│ ├── Actions/
│ │ ├── ExecuteEntityActionCommand.cs
│ │ └── ExecuteEntityActionHandler.cs
│ ├── Discovery/
│ │ └── IWorkflowDiscoveryService.cs
│ ├── Elsa/
│ │ └── IElsaWorkflowClient.cs
│ └── Tenancy/
│ ├── ITenantRepository.cs
│ ├── TenantContext.cs
│ └── TenantProvider.cs
│
├── WorkflowApp.Infrastructure/ # Infrastructure Layer
│ ├── Elsa/
│ │ ├── ElsaSettings.cs
│ │ ├── ElsaTokenService.cs # JWT token management for Elsa
│ │ ├── ElsaAuthenticationHandler.cs
│ │ ├── ElsaWorkflowClient.cs # HTTP client for Elsa API
│ │ ├── IElsaTokenService.cs
│ │ └── WorkflowDiscoveryService.cs
│ ├── Middleware/
│ │ ├── TenantContextMiddleware.cs
│ │ └── TenantContextMiddlewareExtensions.cs
│ ├── Persistence/
│ │ ├── AppDbContext.cs
│ │ ├── Configurations/ # EF Core configurations
│ │ └── Migrations/
│ └── Repositories/
│ ├── TenantRepository.cs
│ └── TriggerRepository.cs
│
├── WorkflowApp.API/ # Presentation Layer
│ ├── Controllers/
│ │ ├── ActionsController.cs # Execute entity actions
│ │ └── AdminController.cs # Manage triggers and workflows
│ ├── Auth/
│ │ ├── Auth0Extensions.cs
│ │ └── Auth0Settings.cs
│ ├── Program.cs # Application entry point
│ ├── appsettings.json
│ └── Dockerfile
│
└── workflow-web/ # Angular Frontend
├── src/
│ ├── app/
│ ├── environments/
│ └── assets/
├── package.json
└── Dockerfile
A Trigger Binding maps an entity action (like Cart.AddItem) to a specific Elsa workflow. Triggers can be:
- Auto-discovered: Automatically extracted from workflow
customProperties.triggerBindingmetadata - Manual overrides: Created by admins to override or customize behavior
// Example: triggerBinding in a workflow's customProperties
{
"customProperties": {
"triggerBinding": {
"entityType": "Cart",
"actionType": "AddItem",
"priority": 100,
"description": "Handles adding items to shopping cart"
}
}
}When multiple triggers exist for the same action, the system uses priority-based resolution:
- Manual triggers always take precedence over auto-discovered triggers
- Higher priority values win (e.g., priority 200 beats priority 100)
- Only active triggers are considered
The application supports multiple tenants (organizations) with full data isolation:
- Each tenant is identified by an Auth0 Organization ID (
org_idclaim) - Tenants are auto-provisioned on first login
- All database queries are filtered by tenant
- Elsa receives tenant context via
X-Tenant-Idheader
Workflows in Elsa can declare their trigger bindings using custom properties. During discovery, the system:
- Fetches all published workflows from Elsa
- Extracts
triggerBindingfromcustomProperties - Creates/updates triggers in the database
- Removes stale triggers for deleted workflows
Contains business entities and interfaces with no external dependencies.
Key Entities:
| Entity | Purpose |
|---|---|
Tenant |
Represents an organization (maps to Auth0 Organization) |
WorkflowTrigger |
Maps entity.action to a workflow definition |
WorkflowExecution |
Tracks execution history (audit trail) |
Key Interfaces:
| Interface | Purpose |
|---|---|
ITenantProvider |
Provides current tenant context |
ITriggerRegistry |
CRUD operations for workflow triggers |
ITenantScoped |
Marker for multi-tenant entities |
Contains business logic, commands, and handlers using MediatR.
Key Classes:
| Class | Purpose |
|---|---|
ExecuteEntityActionCommand |
Command to execute an entity action |
ExecuteEntityActionHandler |
Handles the command, finds trigger, executes workflow |
TenantProvider |
Manages tenant context per request |
Contains external service integrations and data access.
Key Classes:
| Class | Purpose |
|---|---|
ElsaWorkflowClient |
HTTP client for Elsa REST API |
ElsaTokenService |
Manages Elsa authentication tokens with caching |
WorkflowDiscoveryService |
Syncs workflow triggers from Elsa |
TriggerRepository |
Database operations for triggers |
TenantContextMiddleware |
Extracts tenant from JWT claims |
REST API controllers and configuration.
Controllers:
| Controller | Purpose |
|---|---|
ActionsController |
POST /api/actions/execute - Execute entity actions |
AdminController |
Admin endpoints for trigger and workflow management |
Before you begin, ensure you have:
- .NET 8 SDK
- Node.js 18+
- Docker Desktop
- Auth0 Account (for authentication)
Step 1: Clone and navigate to the project
cd "c:\Users\marka_unops\Documents\UNOPS\ELSA\elsa-workflows-demo"Step 2: Start all services
docker-compose up -d --buildThis starts:
- PostgreSQL on port 5432
- Elsa Server + Studio on port 14000
- Workflow API on port 5000
- Angular Frontend on port 4200
Step 3: Verify services are running
docker-compose psStep 4: Import sample workflows
cd workflows
$workflows = @("cart-add-item.json", "cart-remove-item.json", "order-checkout.json", "order-pay.json")
foreach ($workflow in $workflows) {
$json = Get-Content $workflow -Raw
Invoke-RestMethod -Uri "http://localhost:14000/elsa/api/workflow-definitions/import" `
-Method POST -ContentType "application/json" -Body $json
Write-Host "Imported: $workflow"
}Step 5: Access the applications
- Frontend: http://localhost:4200
- Elsa Studio: http://localhost:14000
- API Swagger: http://localhost:5000/swagger
If you want to run the API locally for debugging:
Step 1: Start only infrastructure
docker-compose up -d postgres elsa-server-and-studioStep 2: Configure Auth0
Update src/WorkflowApp.API/appsettings.Development.json:
{
"Auth0": {
"Domain": "your-tenant.auth0.com",
"Audience": "https://api.workflowapp.com"
},
"Elsa": {
"ServerUrl": "http://localhost:14000",
"Username": "admin",
"Password": "password"
}
}Step 3: Run database migrations
cd src/WorkflowApp.API
dotnet ef database update --project ../WorkflowApp.InfrastructureStep 4: Start the API
cd src/WorkflowApp.API
dotnet runStep 5: Start the frontend
cd src/workflow-web
npm install
npm startsequenceDiagram
participant DC as Docker Compose
participant PG as PostgreSQL
participant Elsa as Elsa Server
participant API as Workflow API
participant UI as Angular App
DC->>PG: Start PostgreSQL container
PG-->>DC: Healthy port 5432
DC->>Elsa: Start Elsa Server
Note over Elsa: Waits for PostgreSQL
Elsa->>PG: Connect and initialize
Elsa-->>DC: Healthy port 14000
DC->>API: Start Workflow API
Note over API: Waits for PostgreSQL and Elsa
API->>PG: Run EF Core migrations
PG-->>API: Migrations applied
API-->>DC: Healthy port 5000
DC->>UI: Start Angular container
UI-->>DC: Ready port 4200
Note over DC: All services running
Description: A user performs an action in the frontend, which triggers a workflow execution.
Actors: User, Angular Frontend, API, Elsa Server
Preconditions:
- User is authenticated with Auth0
- Workflow for the action exists and is published in Elsa
- Trigger binding exists (auto-discovered or manual)
sequenceDiagram
participant User
participant UI as Angular Frontend
participant API as Workflow API
participant MW as Tenant Middleware
participant MediatR
participant TR as Trigger Repository
participant Elsa as Elsa Server
participant PG as PostgreSQL
User->>UI: Click Add to Cart
UI->>API: POST /api/actions/execute
Note over API: JWT token in Authorization header
API->>MW: Process request
MW->>MW: Extract org_id from JWT
MW->>PG: Find or create tenant by org_id
PG-->>MW: Tenant context set
MW->>MediatR: ExecuteEntityActionCommand
MediatR->>TR: FindAsync Cart AddItem
TR->>PG: Query triggers by priority
PG-->>TR: WorkflowTrigger
TR-->>MediatR: trigger.WorkflowDefinitionId
Note over MediatR: Build workflow input
MediatR->>Elsa: POST execute workflow
Note over MediatR,Elsa: Bearer token + X-Tenant-Id header
Elsa->>Elsa: Execute workflow activities
Elsa-->>MediatR: workflowInstanceId, status, output
MediatR-->>API: WorkflowExecutionResult
API-->>UI: 200 OK success
UI-->>User: Show success message
Request Example:
POST /api/actions/execute
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
{
"entityType": "Cart",
"actionType": "AddItem",
"entityId": null,
"payload": {
"productId": "PROD-001",
"quantity": 2
}
}Response Example:
{
"success": true,
"workflowInstanceId": "abc123",
"status": "Finished",
"output": {
"success": true,
"message": "Item added to cart",
"cartItem": {
"productId": "PROD-001",
"quantity": 2,
"unitPrice": 29.99,
"totalPrice": 59.98
}
}
}Description: An admin refreshes the trigger bindings by syncing with Elsa to discover new or updated workflows.
Actors: Admin User, Angular Frontend, API, Elsa Server
sequenceDiagram
participant Admin
participant UI as Angular Frontend
participant API as Admin Controller
participant DS as WorkflowDiscoveryService
participant Elsa as Elsa Server
participant TR as Trigger Repository
participant PG as PostgreSQL
Admin->>UI: Click Refresh Discovery
UI->>API: POST /api/admin/discovery/refresh
API->>DS: SyncTenantBindingsAsync
DS->>Elsa: GET workflow-definitions Published
Elsa-->>DS: List of workflow summaries
DS->>Elsa: GET workflow-definitions many-by-id
Note over DS,Elsa: Fetch full definitions with customProperties
Elsa-->>DS: Full workflow definitions
loop For each workflow with triggerBinding
DS->>DS: Extract triggerBinding
DS->>TR: UpsertAutoDiscoveredAsync
alt Manual trigger exists
TR-->>DS: Return existing manual trigger
else No manual trigger
TR->>PG: Insert or Update auto trigger
PG-->>TR: Trigger saved
TR-->>DS: Return auto trigger
end
end
DS->>TR: CleanupStaleAutoTriggersAsync
TR->>PG: Delete triggers for removed workflows
PG-->>TR: Stale triggers removed
DS-->>API: Discovery completed
API-->>UI: 200 OK Discovery completed
UI-->>Admin: Show success notification
Description: An admin creates a manual trigger to override an auto-discovered workflow or create a new action mapping.
Actors: Admin User, Angular Frontend, API
sequenceDiagram
participant Admin
participant UI as Angular Frontend
participant API as Admin Controller
participant TR as Trigger Repository
participant PG as PostgreSQL
Admin->>UI: Fill override form
UI->>API: POST /api/admin/triggers/override
Note over API: Request with entityType, actionType, workflowId
API->>TR: CreateManualOverrideAsync
TR->>PG: Check for existing manual trigger
alt Manual trigger exists
PG-->>TR: Existing trigger found
TR->>PG: UPDATE existing trigger
else No manual trigger
TR->>PG: INSERT new manual trigger
end
PG-->>TR: Trigger saved
TR-->>API: WorkflowTrigger
API-->>UI: 201 Created with trigger details
UI-->>Admin: Show Override created message
Note over Admin: Future actions will use manual override
Description: How a user's request is authenticated and the tenant context is established.
Actors: User, Angular Frontend, Auth0, API
sequenceDiagram
participant User
participant UI as Angular Frontend
participant Auth0
participant API as Workflow API
participant MW as TenantContextMiddleware
participant TP as TenantProvider
participant PG as PostgreSQL
User->>UI: Open application
UI->>Auth0: Redirect to login with organization
Auth0-->>User: Show login page
User->>Auth0: Enter credentials
Auth0->>Auth0: Validate credentials
Auth0-->>UI: Return JWT with org_id claim
Note over UI: JWT contains sub, org_id, email
UI->>API: Any API request with JWT
Note over API: Authorization Bearer token
API->>API: Validate JWT signature with Auth0
API->>MW: Request enters pipeline
MW->>MW: Extract org_id from claims
MW->>TP: SetTenantFromAuth0OrgAsync
TP->>PG: SELECT FROM Tenants by Auth0OrgId
alt Tenant exists
PG-->>TP: Tenant record
else First login auto-provision
TP->>PG: INSERT new Tenant
PG-->>TP: New tenant created
end
TP->>TP: Store tenant in HttpContext.Items
TP-->>MW: Tenant context set
MW->>API: Continue to controller
Note over API: All DB queries filtered by TenantId
Description: How the API authenticates with Elsa Server using username/password and caches the JWT token.
Actors: API, ElsaTokenService, Elsa Server
sequenceDiagram
participant Handler as ExecuteEntityActionHandler
participant Client as ElsaWorkflowClient
participant TS as ElsaTokenService
participant Cache as MemoryCache
participant Elsa as Elsa Server
Handler->>Client: ExecuteWorkflowAsync
Client->>TS: GetTokenAsync
TS->>Cache: Check for cached token
alt Token in cache valid
Cache-->>TS: Return cached token
TS-->>Client: Bearer token
else Token not in cache or expired
TS->>TS: Acquire semaphore
TS->>Cache: Double-check cache
alt Still no token
TS->>Elsa: POST /elsa/api/identity/login
Elsa-->>TS: accessToken returned
TS->>Cache: Cache token 23 hours TTL
end
TS->>TS: Release semaphore
TS-->>Client: Bearer token
end
Client->>Elsa: POST execute workflow
Note over Client,Elsa: Authorization Bearer token
alt Token valid
Elsa-->>Client: 200 OK with result
else Token expired 401
Client->>TS: ForceRefreshAsync
TS->>Cache: Clear cached token
TS->>Elsa: POST identity login
Elsa-->>TS: New token
TS-->>Client: New Bearer token
Client->>Elsa: Retry request with new token
Elsa-->>Client: 200 OK with result
end
Client-->>Handler: WorkflowExecutionResponse
Description: An admin enables or disables a trigger, controlling whether it can be used for action execution.
sequenceDiagram
participant Admin
participant UI as Angular Frontend
participant API as Admin Controller
participant TR as Trigger Repository
participant PG as PostgreSQL
Admin->>UI: Toggle trigger active state
UI->>API: PATCH /api/admin/triggers/id/active
API->>TR: GetByIdAsync id
TR->>PG: SELECT trigger
PG-->>TR: WorkflowTrigger
alt Trigger not found
TR-->>API: null
API-->>UI: 404 Not Found
else Trigger found
API->>TR: SetActiveAsync id false
TR->>PG: UPDATE SET IsActive false
PG-->>TR: Updated
TR-->>API: Success
API-->>UI: 204 No Content
UI-->>Admin: Show Trigger disabled message
end
Note over Admin: Disabled triggers skipped during execution
Description: A user completes a full shopping flow: add item, checkout, pay.
sequenceDiagram
participant User
participant UI as Angular Frontend
participant API as Workflow API
participant Elsa as Elsa Server
Note over User,Elsa: Step 1 - Add Item to Cart
User->>UI: Add product to cart
UI->>API: POST execute Cart.AddItem
API->>Elsa: Execute cart-add-item workflow
Elsa-->>API: success with cartItem
API-->>UI: Item added
UI-->>User: Show updated cart
Note over User,Elsa: Step 2 - Checkout
User->>UI: Click Checkout
UI->>API: POST execute Order.Checkout
API->>Elsa: Execute order-checkout workflow
Elsa-->>API: success with order details
API-->>UI: Order created
UI-->>User: Show order summary
Note over User,Elsa: Step 3 - Pay
User->>UI: Click Pay Now
UI->>API: POST execute Order.Pay
API->>Elsa: Execute order-pay workflow
Note over Elsa: 2 second processing delay
Elsa-->>API: success with payment details
API-->>UI: Payment complete
UI-->>User: Show confirmation
POST /api/actions/execute
Authorization: Bearer <token>
Content-Type: application/json
{
"entityType": "Cart", // Required: Entity type (e.g., "Cart", "Order")
"actionType": "AddItem", // Required: Action type (e.g., "AddItem", "Pay")
"entityId": "guid", // Optional: ID of existing entity
"payload": { // Optional: Additional data for workflow
"productId": "PROD-001",
"quantity": 2
}
}Responses:
200 OK: Action executed successfully400 Bad Request: Missing required fields401 Unauthorized: Invalid or missing JWT404 Not Found: No trigger configured for this action
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/admin/triggers |
List all triggers for current tenant |
| GET | /api/admin/triggers/{id} |
Get trigger by ID |
| POST | /api/admin/triggers/override |
Create manual trigger override |
| PATCH | /api/admin/triggers/{id}/active |
Enable/disable trigger |
| DELETE | /api/admin/triggers/{id} |
Delete a trigger |
| GET | /api/admin/workflows |
List workflows from Elsa |
| POST | /api/admin/discovery/refresh |
Sync triggers from Elsa |
GET /healthReturns: {"Status": "Healthy", "Timestamp": "2026-01-13T00:00:00Z"}
Cause: No trigger binding exists for the requested entity.action combination.
Solution:
- Check if the workflow exists in Elsa Studio (http://localhost:14000)
- Verify the workflow has
customProperties.triggerBindingset correctly - Click "Refresh Discovery" in the admin panel
- Check if the trigger is active in the triggers list
Cause: JWT token is invalid or missing required claims.
Solution:
- Verify Auth0 configuration in
appsettings.json - Ensure the user belongs to an Auth0 Organization
- Check that the JWT contains the
org_idclaim - Verify the audience matches your API identifier
Cause: Cannot connect to Elsa Server or authentication failed.
Solution:
- Verify Elsa is running:
docker-compose ps - Check Elsa URL in configuration:
Elsa:ServerUrl - Verify Elsa credentials:
Elsa:UsernameandElsa:Password - Check Elsa logs:
docker-compose logs elsa-server-and-studio
Cause: PostgreSQL not accessible or database doesn't exist.
Solution:
- Verify PostgreSQL is running:
docker-compose ps - Check connection string in
appsettings.json - Ensure the
workflow_appdatabase exists - Run migrations:
dotnet ef database update
Cause: Workflows are not published or don't have trigger bindings.
Solution:
- In Elsa Studio, ensure workflows are Published (not just saved)
- Verify
customProperties.triggerBindingis set on each workflow - Check the API logs for discovery errors
- Try re-importing the sample workflows from the
workflows/folder
# Start all services
docker-compose up -d
# Stop all services
docker-compose down
# View logs
docker-compose logs -f workflow-api
# Restart a specific service
docker-compose restart workflow-api
# Run migrations (development mode)
cd src/WorkflowApp.API
dotnet ef database update --project ../WorkflowApp.Infrastructure
# Import workflows
cd workflows
Invoke-RestMethod -Uri "http://localhost:14000/elsa/api/workflow-definitions/import" `
-Method POST -ContentType "application/json" -Body (Get-Content "cart-add-item.json" -Raw)
# Test API health
Invoke-RestMethod -Uri "http://localhost:5000/health"| Term | Definition |
|---|---|
| Trigger | A mapping between an entity.action pair and a workflow |
| Entity Type | The type of object being acted upon (e.g., Cart, Order) |
| Action Type | The operation being performed (e.g., AddItem, Checkout) |
| Workflow Definition | The blueprint for a workflow in Elsa |
| Workflow Instance | A running execution of a workflow definition |
| Trigger Binding | Metadata in a workflow that declares which action it handles |
| Tenant | An isolated organization/customer in the multi-tenant system |
| Auto-discovered Trigger | A trigger automatically created from workflow metadata |
| Manual Override | An admin-created trigger that takes priority over auto-discovered ones |
Last updated: January 2026