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 IDPOST /workflows/tasks- Create a new taskPUT /workflows/tasks/{id}- Update an existing taskDELETE /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.
Create the List Tasks Workflow
Open Elsa Studio and navigate to Workflows
Click Create Workflow
Name it
ListTasksSet the workflow as Published when ready
Add Required Activities
Add the following activities to your workflow:
HTTP Endpoint - To receive the request
Set Variable - To extract query parameters
Set Variable - To create a filtered task list
Write HTTP Response - To return the results
Create Workflow Variables
Create the following variables:
QueryData
ObjectDictionary
Workflow Instance
StatusFilter
string
Workflow Instance
Tasks
Object
Workflow Instance
Configure HTTP Endpoint
Configure the HTTP Endpoint activity:
Path
tasks
Default
Supported Methods
GET
Default
Query String Data
QueryData
Trigger Workflow
Checked
Extract Query Parameters
Configure the first Set Variable activity to extract the status filter:
Variable
StatusFilter
Default
Value
{{ Variables.QueryData.status ?? "all" }}
Liquid
This extracts the status query parameter (e.g., ?status=active) or defaults to "all".
Create Mock Task Data
Configure the second Set Variable activity to create sample task data:
Variable
Tasks
Default
Value
See code below
C#
C# Expression:
Return Response
Configure the Write HTTP Response activity:
Status Code
OK
Default
Content
Variables.Tasks
JavaScript
Content Type
application/json
Default
Test the Workflow
Publish the workflow
Test with different query parameters:
GET https://localhost:5001/workflows/tasks- Returns all tasksGET https://localhost:5001/workflows/tasks?status=active- Returns only active tasksGET https://localhost:5001/workflows/tasks?status=completed- Returns completed tasks
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:
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.
Create the Get Task Workflow
Create a new workflow named
GetTaskThis workflow will handle requests like
GET /workflows/tasks/1
Create Variables
RouteData
ObjectDictionary
Workflow Instance
TaskId
string
Workflow Instance
Task
Object
Workflow Instance
Configure HTTP Endpoint
Path
tasks/{id}
Default
Supported Methods
GET
Default
Route Data
RouteData
Trigger Workflow
Checked
Extract Task ID
Add a Set Variable activity:
Variable
TaskId
Default
Value
{{ Variables.RouteData.id }}
Liquid
Find Task
Add another Set Variable activity with branching logic:
Variable
Task
Default
Value
See code below
C#
C# Expression:
Add Conditional Response
Add a Decision activity to check if the task was found:
Condition
Variables.Task != null
C#
Connect two Write HTTP Response activities to the Decision outcomes:
For "True" outcome (Task Found):
Status Code
OK
Default
Content
Variables.Task
JavaScript
Content Type
application/json
Default
For "False" outcome (Task Not Found):
Status Code
NotFound
Default
Content
{"error": "Task not found", "taskId": "{{Variables.TaskId}}"}
Liquid
Content Type
application/json
Default
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.
Create the Create Task Workflow
Create a new workflow named CreateTask
Create Variables
RequestBody
Object
Workflow Instance
NewTask
Object
Workflow Instance
ValidationErrors
Object
Workflow Instance
Configure HTTP Endpoint
Path
tasks
Default
Supported Methods
POST
Default
Parsed Content
RequestBody
Trigger Workflow
Checked
The HTTP Endpoint automatically parses JSON request bodies into the Parsed Content output.
Validate Request Body
Add a Set Variable activity to validate the input:
Variable
ValidationErrors
Default
Value
See code below
C#
C# Expression:
Add Validation Decision
Add a Decision activity:
Condition
Variables.ValidationErrors == null
C#
Create Task (Valid Input)
For the "True" outcome, add a Set Variable activity:
Variable
NewTask
Default
Value
See code below
C#
C# Expression:
Then add a Write HTTP Response activity:
Status Code
Created
Default
Content
Variables.NewTask
JavaScript
Content Type
application/json
Default
Add a custom header:
Name:
LocationValue:
/workflows/tasks/{{Variables.NewTask.Id}}(Liquid)
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.
Return Validation Errors (Invalid Input)
For the "False" outcome, add a Write HTTP Response activity:
Status Code
BadRequest
Default
Content
Variables.ValidationErrors
JavaScript
Content Type
application/json
Default
Test the Workflow
Test with valid data:
Test with invalid data:
Part 4: Creating a PUT Endpoint for Updates
Let's create a workflow that handles PUT requests to update existing tasks.
Create the Update Task Workflow
Create a new workflow named UpdateTask
Create Variables
RouteData
ObjectDictionary
Workflow Instance
RequestBody
Object
Workflow Instance
TaskId
string
Workflow Instance
ExistingTask
Object
Workflow Instance
UpdatedTask
Object
Workflow Instance
Configure HTTP Endpoint
Path
tasks/{id}
Default
Supported Methods
PUT
Default
Route Data
RouteData
Parsed Content
RequestBody
Trigger Workflow
Checked
Extract Task ID
Add a Set Variable activity:
Variable
TaskId
Default
Value
{{ Variables.RouteData.id }}
Liquid
Find Existing Task
Add a Set Variable activity:
Variable
ExistingTask
Default
Value
See code below
C#
C# Expression:
Add Decision for Task Existence
Add a Decision activity:
Condition
Variables.ExistingTask != null
C#
Update Task (If Found)
For the "True" outcome, add a Set Variable activity:
Variable
UpdatedTask
Default
Value
See code below
C#
C# Expression:
Then add a Write HTTP Response activity:
Status Code
OK
Default
Content
Variables.UpdatedTask
JavaScript
Content Type
application/json
Default
Return Not Found (If Task Doesn't Exist)
For the "False" outcome, add a Write HTTP Response activity:
Status Code
NotFound
Default
Content
{"error": "Task not found", "taskId": "{{Variables.TaskId}}"}
Liquid
Content Type
application/json
Default
Test the Workflow
Test updating an existing task:
Test updating a non-existent task:
Part 5: Creating a DELETE Endpoint
Let's complete our CRUD operations with a DELETE endpoint.
Create the Delete Task Workflow
Create a new workflow named DeleteTask
Create Variables
RouteData
ObjectDictionary
Workflow Instance
TaskId
string
Workflow Instance
TaskExists
bool
Workflow Instance
Configure HTTP Endpoint
Path
tasks/{id}
Default
Supported Methods
DELETE
Default
Route Data
RouteData
Trigger Workflow
Checked
Extract and Validate Task ID
Add a Set Variable activity to extract the ID:
Variable
TaskId
Default
Value
{{ Variables.RouteData.id }}
Liquid
Then add another Set Variable activity to check if task exists:
Variable
TaskExists
Default
Value
See code below
C#
C# Expression:
Add Decision
Add a Decision activity:
Condition
Variables.TaskExists
C#
Return Success (If Deleted)
For the "True" outcome, add a Write HTTP Response activity:
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.
Return Not Found (If Task Doesn't Exist)
For the "False" outcome, add a Write HTTP Response activity:
Status Code
NotFound
Default
Content
{"error": "Task not found", "taskId": "{{Variables.TaskId}}"}
Liquid
Content Type
application/json
Default
Test the Workflow
Test deleting an existing task:
Test deleting a non-existent task:
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:
Create Variables
Headers
ObjectDictionary
Workflow Instance
AuthToken
string
Workflow Instance
UserAgent
string
Workflow Instance
Configure HTTP Endpoint
Headers
Headers
Extract Header Values
Add Set Variable activities to extract specific headers:
For Authorization Header:
Variable
AuthToken
Default
Value
{{ Variables.Headers.Authorization ?? "No token provided" }}
Liquid
For User-Agent Header:
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:
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:
Use Fault Activity
Wrap risky operations in a Fault activity to catch exceptions:
Add a Fault activity
Inside the Fault activity, add activities that might fail (e.g., HTTP Request to external API)
Connect the Faulted outcome to error handling logic
Handle Fault
Create a Set Variable activity to process the error:
Variable
ErrorResponse
Default
Value
See code below
C#
C# Expression:
Return Error Response
Add a Write HTTP Response activity:
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:
Pattern 3: Custom Error Status Codes
Use appropriate HTTP status codes for different error scenarios:
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
CORS Headers
Enable Cross-Origin Resource Sharing (CORS) for browser-based clients:
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
CORS Security
Never use
*forAccess-Control-Allow-Originin production, especially with credentialsAlways 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
Pagination
Implement pagination for list endpoints:
Rate Limiting
Track and limit request rates per client:
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.
Part 9: Testing Your HTTP Workflows
Using Postman
Create a Collection: Organize all your workflow endpoints
Set Environment Variables: Configure base URL, auth tokens
Write Tests: Add test scripts to validate responses
Example Postman test script:
Using cURL
Test your endpoints from the command line:
Using HTTP Files (REST Client)
Create a .http file for testing (replace {{baseUrl}} with your server URL):
Environment Variables
Use variables in .http files to easily switch between environments:
Development:
@baseUrl = https://localhost:5001Staging:
@baseUrl = https://staging.example.comProduction:
@baseUrl = https://api.example.com
Automated Testing with xUnit
Create integration tests for your workflows:
Part 10: Debugging and Troubleshooting
Using Elsa Studio for Debugging
Navigate to Workflow Instances: View all executions of your workflow
Inspect Activity Execution: See inputs/outputs for each activity
Check Journal Entries: View the execution timeline
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/jsonheaderEnsure 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:
Write HTTP Response Activity:
Enabling Detailed Logging
Configure logging in your Elsa Server's appsettings.json:
Best Practices
1. Use Consistent Response Formats
Always return JSON in a consistent structure:
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:
HTTP Endpoint (POST
/workflows/tasks)Outputs: Headers, RequestBody
Extract Auth Token
AuthToken = Headers.Authorization
Validate Authentication
Check if token is valid
Branch: Authenticated / Unauthorized
Validate Request Body (if authenticated)
Check required fields
Validate formats
Check business rules
Decision: Valid Input?
True: Create task
False: Return validation errors
Create Task (if valid)
Generate ID
Set timestamps
Prepare response
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: Integrate with external services
Custom Activities: Create reusable workflow components
Authentication: Secure your workflows
Testing & Debugging: Advanced debugging techniques
Distributed Hosting: Scale your workflows
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