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 project up and running

  • 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)

New to Elsa?

If you haven't set up Elsa yet, check out our Getting Started guide and Docker Quickstart for the fastest way to get up and running.

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.

1

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

2

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

3

Create Workflow Variables

Create the following variables:

Name
Type
Storage

QueryData

ObjectDictionary

Workflow Instance

StatusFilter

string

Workflow Instance

Tasks

Object

Workflow Instance

4

Configure HTTP Endpoint

Configure the HTTP Endpoint activity:

Property
Value
Syntax

Path

tasks

Default

Supported Methods

GET

Default

5

Extract Query Parameters

Configure the first Set Variable activity to extract the status filter:

Property
Value
Syntax

Variable

StatusFilter

Default

Value

{{ Variables.QueryData.status ?? "all" }}

Liquid

This extracts the status query parameter (e.g., ?status=active) or defaults to "all".

6

Create Mock Task Data

Configure the second Set Variable activity to create sample task data:

Property
Value
Syntax

Variable

Tasks

Default

Value

See code below

C#

C# Expression:

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();
7

Return Response

Configure the Write HTTP Response activity:

Property
Value
Syntax

Status Code

OK

Default

Content

Variables.Tasks

JavaScript

Content Type

application/json

Default

8

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

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.

1

Create the Get Task Workflow

  1. Create a new workflow named GetTask

  2. This workflow will handle requests like GET /workflows/tasks/1

2

Create Variables

Name
Type
Storage

RouteData

ObjectDictionary

Workflow Instance

TaskId

string

Workflow Instance

Task

Object

Workflow Instance

3

Configure HTTP Endpoint

Property
Value
Syntax

Path

tasks/{id}

Default

Supported Methods

GET

Default

4

Extract Task ID

Add a Set Variable activity:

Property
Value
Syntax

Variable

TaskId

Default

Value

{{ Variables.RouteData.id }}

Liquid

5

Find Task

Add another Set Variable activity with branching logic:

Property
Value
Syntax

Variable

Task

Default

Value

See code below

C#

C# Expression:

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);
6

Add Conditional Response

Add a Decision activity to check if the task was found:

Property
Value
Syntax

Condition

Variables.Task != null

C#

Connect two Write HTTP Response activities to the Decision outcomes:

For "True" outcome (Task Found):

Property
Value
Syntax

Status Code

OK

Default

Content

Variables.Task

JavaScript

Content Type

application/json

Default

For "False" outcome (Task Not Found):

Property
Value
Syntax

Status Code

NotFound

Default

Content

{"error": "Task not found", "taskId": "{{Variables.TaskId}}"}

Liquid

Content Type

application/json

Default

7

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)

Part 3: Creating a POST Endpoint for Creating Resources

Let's create a workflow that handles POST requests to create new tasks.

1

Create the Create Task Workflow

Create a new workflow named CreateTask

2

Create Variables

Name
Type
Storage

RequestBody

Object

Workflow Instance

NewTask

Object

Workflow Instance

ValidationErrors

Object

Workflow Instance

3

Configure HTTP Endpoint

Property
Value
Syntax

Path

tasks

Default

Supported Methods

POST

Default

The HTTP Endpoint automatically parses JSON request bodies into the Parsed Content output.

4

Validate Request Body

Add a Set Variable activity to validate the input:

Property
Value
Syntax

Variable

ValidationErrors

Default

Value

See code below

C#

C# Expression:

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;
5

Add Validation Decision

Add a Decision activity:

Property
Value
Syntax

Condition

Variables.ValidationErrors == null

C#

6

Create Task (Valid Input)

For the "True" outcome, add a Set Variable activity:

Property
Value
Syntax

Variable

NewTask

Default

Value

See code below

C#

C# Expression:

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:

Property
Value
Syntax

Status Code

Created

Default

Content

Variables.NewTask

JavaScript

Content Type

application/json

Default

7

Return Validation Errors (Invalid Input)

For the "False" outcome, add a Write HTTP Response activity:

Property
Value
Syntax

Status Code

BadRequest

Default

Content

Variables.ValidationErrors

JavaScript

Content Type

application/json

Default

8

Test the Workflow

Test with valid data:

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:

curl -X POST https://localhost:5001/workflows/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "status": "invalid"
  }'

Part 4: Creating a PUT Endpoint for Updates

Let's create a workflow that handles PUT requests to update existing tasks.

1

Create the Update Task Workflow

Create a new workflow named UpdateTask

2

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

3

Configure HTTP Endpoint

Property
Value
Syntax

Path

tasks/{id}

Default

Supported Methods

PUT

Default

4

Extract Task ID

Add a Set Variable activity:

Property
Value
Syntax

Variable

TaskId

Default

Value

{{ Variables.RouteData.id }}

Liquid

5

Find Existing Task

Add a Set Variable activity:

Property
Value
Syntax

Variable

ExistingTask

Default

Value

See code below

C#

C# Expression:

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);
6

Add Decision for Task Existence

Add a Decision activity:

Property
Value
Syntax

Condition

Variables.ExistingTask != null

C#

7

Update Task (If Found)

For the "True" outcome, add a Set Variable activity:

Property
Value
Syntax

Variable

UpdatedTask

Default

Value

See code below

C#

C# Expression:

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:

Property
Value
Syntax

Status Code

OK

Default

Content

Variables.UpdatedTask

JavaScript

Content Type

application/json

Default

8

Return Not Found (If Task Doesn't Exist)

For the "False" outcome, add a Write HTTP Response activity:

Property
Value
Syntax

Status Code

NotFound

Default

Content

{"error": "Task not found", "taskId": "{{Variables.TaskId}}"}

Liquid

Content Type

application/json

Default

9

Test the Workflow

Test updating an existing task:

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:

curl -X PUT https://localhost:5001/workflows/tasks/999 \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Updated Task"
  }'

Part 5: Creating a DELETE Endpoint

Let's complete our CRUD operations with a DELETE endpoint.

1

Create the Delete Task Workflow

Create a new workflow named DeleteTask

2

Create Variables

Name
Type
Storage

RouteData

ObjectDictionary

Workflow Instance

TaskId

string

Workflow Instance

TaskExists

bool

Workflow Instance

3

Configure HTTP Endpoint

Property
Value
Syntax

Path

tasks/{id}

Default

Supported Methods

DELETE

Default

4

Extract and Validate Task ID

Add a Set Variable activity to extract the ID:

Property
Value
Syntax

Variable

TaskId

Default

Value

{{ Variables.RouteData.id }}

Liquid

Then add another Set Variable activity to check if task exists:

Property
Value
Syntax

Variable

TaskExists

Default

Value

See code below

C#

C# Expression:

var existingTaskIds = new[] { 1, 2, 3, 4, 5 };
var taskId = int.Parse(Variables.TaskId);
return existingTaskIds.Contains(taskId);
5

Add Decision

Add a Decision activity:

Property
Value
Syntax

Condition

Variables.TaskExists

C#

6

Return Success (If Deleted)

For the "True" outcome, add a Write HTTP Response activity:

Property
Value
Syntax

Status Code

NoContent

Default

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.

7

Return Not Found (If Task Doesn't Exist)

For the "False" outcome, add a Write HTTP Response activity:

Property
Value
Syntax

Status Code

NotFound

Default

Content

{"error": "Task not found", "taskId": "{{Variables.TaskId}}"}

Liquid

Content Type

application/json

Default

8

Test the Workflow

Test deleting an existing task:

curl -X DELETE https://localhost:5001/workflows/tasks/1

Test deleting a non-existent task:

curl -X DELETE https://localhost:5001/workflows/tasks/999

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:

1

Create Variables

Name
Type
Storage

Headers

ObjectDictionary

Workflow Instance

AuthToken

string

Workflow Instance

UserAgent

string

Workflow Instance

2

Configure HTTP Endpoint

Property
Value

Headers

Headers

3

Extract Header Values

Add Set Variable activities to extract specific headers:

For Authorization Header:

Property
Value
Syntax

Variable

AuthToken

Default

Value

{{ Variables.Headers.Authorization ?? "No token provided" }}

Liquid

For User-Agent Header:

Property
Value
Syntax

Variable

UserAgent

Default

Value

{{ Variables.Headers["User-Agent"] ?? "Unknown" }}

Liquid

Setting Response Headers

To set custom response headers, configure the Write HTTP Response activity:

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

Part 7: Error Handling Strategies

Implement robust error handling to make your workflows production-ready.

Pattern 1: Try-Catch with Fault Activity

Create a workflow that handles exceptions gracefully:

1

Use Fault Activity

Wrap risky operations in a Fault activity to catch exceptions:

  1. Add a Fault activity

  2. Inside the Fault activity, add activities that might fail (e.g., HTTP Request to external API)

  3. Connect the Faulted outcome to error handling logic

2

Handle Fault

Create a Set Variable activity to process the error:

Property
Value
Syntax

Variable

ErrorResponse

Default

Value

See code below

C#

C# Expression:

return new
{
    Error = "An error occurred while processing your request",
    Timestamp = DateTime.UtcNow,
    RequestId = Guid.NewGuid().ToString()
};
3

Return Error Response

Add a Write HTTP Response activity:

Property
Value
Syntax

Status Code

InternalServerError

Default

Content

Variables.ErrorResponse

JavaScript

Content Type

application/json

Default

Pattern 2: Validation and Early Returns

Validate input early and return appropriate error responses:

// 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:

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

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:

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

Pagination

Implement pagination for list endpoints:

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:

// 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:

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:

# 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):

@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

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

Automated Testing with xUnit

Create integration tests for your workflows:

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:

// Add support for OPTIONS method for CORS preflight
SupportedMethods = new[] { HttpMethods.Get, HttpMethods.Post, HttpMethods.Options }

Write HTTP Response Activity:

// 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:

{
  "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:

// 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:

Resources

Feedback

Found an issue or have suggestions for improving this tutorial? Please open an issue on our GitHub repository.

Happy workflow building! 🚀

Last updated