Enterprise Workflow Automation with Logic Apps
Why Logic Apps for Enterprise Automation
Azure Logic Apps provide a powerful, low-code platform for building enterprise workflows:
- Visual workflow design - No code required, easy to understand
- 150+ connectors - Integrate with Azure services, SaaS, and on-premises
- Enterprise-grade - Built-in monitoring, reliability, and scalability
- Event-driven - Trigger on schedules, events, or manual inputs
- Error handling - Built-in retry, compensation, and recovery
However, building production-ready Logic Apps requires understanding patterns for error handling, parallel execution, and complex integrations.
Understanding Logic Apps Architecture
Workflow Components
┌─────────────────────────────────────────────────────────────────────────────┐
│ Logic Apps Architecture │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ TRIGGERS │
│ - HTTP Request - Timer/Schedule - Service Bus Queue │
│ - Blob Storage - Azure Event Grid - API Management │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ ACTIONS │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Control │ │ Data │ │ Connectors │ │ Custom │ │
│ │ - If │ │ - Parse │ │ - Azure │ │ - Azure │ │
│ │ - Switch │ │ - Compose │ │ Functions│ │ Functions│ │
│ │ - ForEach │ │ - Select │ │ - Dynamics │ │ HTTP │ │
│ │ - Until │ │ - Filter │ │ 365 │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Scopes │ │ Errors │ │ Variables │ │ Inline │ │
│ │ - Group │ │ - Throw │ │ - Init │ │ Code │ │
│ │ actions │ │ - Scope │ │ - Set │ │ (Preview) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ CONDITIONAL EXECUTION │
│ │
│ "Run After" settings - control execution order based on previous results │
│ - hasSucceeded - hasFailed - hasTimedOut - isSkipped │
└─────────────────────────────────────────────────────────────────────────────┘
Step 1: Error Handling Patterns
Try-Catch Pattern Using Scopes
The most important pattern for production Logic Apps is the try-catch using Scopes.
{
"definition": {
"triggers": {
"manual": {
"type": "Request",
"kind": "Http"
}
},
"actions": {
// MAIN PROCESSING - "Try" scope
"Try_Scope": {
"type": "Scope",
"actions": {
"Get_Orders": {
"type": "Query",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['sql']['connectionId']"
}
},
"database": "OrdersDB",
"query": "SELECT * FROM Orders WHERE Status = 'Pending'"
}
},
"Process_Each_Order": {
"type": "Foreach",
"foreach": "@body('Get_Orders')",
"actions": {
"Create_Invoice": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['dynamics365']['connectionId']"
}
},
"method": "post",
"path": "/api/data/v9.0/invoices",
"body": {
"name": "Invoice for Order @items('Process_Each_Order').OrderId",
"customerid": "@items('Process_Each_Order').CustomerId"
}
}
},
"Update_Order_Status": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['sql']['connectionId']"
}
},
"method": "update",
"path": "/datasets/default/tables/Orders/items",
"body": {
"Id": "@items('Process_Each_Order').OrderId",
"Status": "Processed"
}
}
}
}
},
"Send_Success_Notification": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['outlook']['connectionId']"
}
},
"method": "post",
"path": "/v2/Mail",
"body": {
"Subject": "Order Processing Complete",
"To": "admin@company.com",
"Body": "Processed @length(body('Process_Each_Order')) orders successfully"
}
}
}
}
},
// ERROR HANDLING - "Catch" scope - runs if Try fails
"Catch_Scope": {
"type": "Scope",
"runAfter": {
"Try_Scope": {
"failed": true
}
},
"actions": {
"Log_Error": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['azureblob']['connectionId']"
}
},
"method": "post",
"path": "/datasets/default/files",
"body": {
"folderPath": "/errors",
"name": "error-@{utcNow()}.json",
"body": {
"error": "@actions('Try_Scope')?['error']",
"timestamp": "@utcNow()"
}
}
}
},
"Send_Error_Alert": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['outlook']['connectionId']"
}
},
"method": "post",
"path": "/v2/Mail",
"body": {
"Subject": "URGENT: Order Processing Failed",
"To": "oncall@company.com",
"Body": "Error: @actions('Try_Scope')?['error']?.message"
}
}
},
"Create_Support_Ticket": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['dynamics365']['connectionId']"
}
},
"method": "post",
"path": "/api/data/v9.0/incidents",
"body": {
"title": "Order Processing Failure",
"description": "@actions('Try_Scope')?['error']?.message"
}
}
}
}
}
}
}
}
Why Scopes for Error Handling?
Without Scopes: With Scopes:
┌──────────────────────────────────┐ ┌──────────────────────────────────┐
│ Action1 ──▶ Action2 ──▶ Action3 │ │ Try_Scope: │
│ │ │ │ Action1 ──▶ Action2 ──▶ Action3│
│ ▼ │ └──────────────┬───────────────────┘
│ Error │ │ hasFailed
│ handler │ ▼
└──────────────────────────────────┘ │ Catch_Scope: │
│ Error handling │
Problems: └──────────────────────────────────┘
- Can only track single action failure
- No way to group related actions
- Can't handle partial failures
- Complex to maintain Benefits:
- Group related actions
- Track scope-level failures
- Handle errors at any level
- Easy to understand and maintain
Retry Policies
{
"Create_Invoice": {
"type": "ApiConnection",
"inputs": {
"host": { "connection": { "name": "@parameters('$connections')['dynamics365']['connectionId']" } },
"method": "post",
"path": "/api/data/v9.0/invoices"
},
"runAfter": {},
"retryPolicy": {
"type": "exponential",
"interval": "PT5S", // Initial interval: 5 seconds
"count": 4, // Number of retries
"minimumInterval": "PT1S", // Minimum: 1 second
"maximumInterval": "PT30S" // Maximum: 30 seconds
}
}
}
Step 2: Parallel Execution Patterns
ForEach with Concurrency Control
{
"Process_Orders_Parallel": {
"type": "Foreach",
"foreach": "@body('Get_Orders')",
"operationOptions": "Sequential",
"runtimeConfiguration": {
"concurrency": {
"value": 10 // Process 10 items in parallel
}
},
"actions": {
"Process_Order": { ... }
}
}
}
Why Concurrency Control Matters
Sequential (default): Parallel (concurrency: 10):
┌──────────────────────────┐ ┌──────────────────────────┐
│ Item 1 ──▶ │ │ Item 1 ──┐ │
│ Item 2 ──▶ │ │ Item 2 ──┼──▶ (10 at │
│ Item 3 ──▶ │ │ Item 3 ──┤ once) │
│ ... │ │ ... │ │
│ │ │ Item 10 ─┘ │
│ Time: 10 items × 5s │ │ │
│ = 50 seconds │ │ Time: 10 items × 5s /10 │
│ │ │ = 5 seconds │
└──────────────────────────┘ └──────────────────────────┘
10x faster with parallel processing!
BUT: Be careful with:
- API rate limits
- Database connections
- External service quotas
Parallel Branch Execution
{
"Process_Order_Complete": {
"type": "Scope",
"actions": {
"Get_Customer": { "type": "ApiConnection", "inputs": { ... } },
"Get_Products": { "type": "ApiConnection", "inputs": { ... } },
"Get_Shipping_Info": { "type": "ApiConnection", "inputs": { ... } }
}
}
}
Step 3: Complex Integration Patterns
Conditional Routing with Switch
{
"Route_By_Order_Type": {
"type": "Switch",
"expression": "@triggerBody()['OrderType']",
"cases": {
"standard": {
"case": "standard",
"actions": { "Process_Standard": { ... } }
},
"express": {
"case": "express",
"actions": { "Process_Express": { ... } }
}
},
"default": {
"actions": { "Log_Unknown_Type": { "type": "Compose", "inputs": "Unknown order type" } }
}
}
}
Until Loop for Long-Running Operations
{
"Wait_For_Confirmation": {
"type": "Until",
"expression": "@variables('confirmed')",
"limit": {
"count": 100,
"timeout": "PT1H"
},
"do": {
"actions": {
"Check_Confirmation": { "type": "Http", "method": "GET", "uri": "..." },
"Set_Confirmed": { "type": "SetVariable", "inputs": { "name": "confirmed", "value": "..." } },
"Wait_Before_Retry": { "type": "Wait", "inputs": { "interval": 30 } }
}
}
}
}
Step 4: Advanced Data Transformation
Parse JSON and Compose
{
"Parse_Order_Data": {
"type": "ParseJson",
"inputs": {
"content": "@triggerBody()",
"schema": {
"type": "object",
"properties": {
"orderId": { "type": "string" },
"customer": { "type": "object", "properties": { "name": { "type": "string" } } },
"items": { "type": "array", "items": { "type": "object", "properties": { "productId": { "type": "string" } } } }
}
}
}
},
"Transform_To_Invoice": {
"type": "Compose",
"inputs": {
"invoiceNumber": "INV-@{body('Parse_Order_Data').orderId}",
"customerName": "@body('Parse_Order_Data').customer.name",
"total": "@multiply(body('Parse_Order_Data').total, 1.1)"
}
}
}
Select and Filter Arrays
{
"Extract_Order_Summary": {
"type": "Select",
"inputs": {
"from": "@body('Get_Orders')",
"select": {
"OrderId": "@item()['OrderId']",
"Total": "@item()['TotalAmount']"
}
}
},
"Get_High_Value_Orders": {
"type": "Filter",
"inputs": {
"array": "@body('Get_Orders')",
"where": "@greaterOrEquals(item()['TotalAmount'], 1000)"
}
}
}
Step 5: Securing Logic Apps
Managed Identities
{
"trigger": {
"type": "Http",
"inputs": {
"authentication": { "type": "ManagedServiceIdentity" }
}
},
"actions": {
"Call_Secure_API": {
"type": "Http",
"inputs": {
"method": "GET",
"uri": "https://secure-api.company.com/data",
"authentication": { "type": "ManagedServiceIdentity", "audience": "https://secure-api.company.com" }
}
}
}
}
Best Practices Summary
| Practice | Why | Implementation |
|---|---|---|
| Use Scopes | Group actions for error handling | Try-Catch pattern |
| Retry Policies | Handle transient failures | Exponential backoff |
| Concurrency Control | Optimize performance | Balance with API limits |
| Logging | Debug and monitor | Add compose actions |
| Managed Identity | Secure access | No credentials in code |
Conclusion
Enterprise Logic Apps require:
- Proper error handling - Use Scopes with try-catch pattern
- Parallel execution - Control concurrency for optimal performance
- State management - Use variables and careful state tracking
- Security - Use managed identities and Key Vault
- Monitoring - Add logging at key points
Key takeaways:
- Always use Scopes for error handling
- Set retry policies on all external calls
- Use concurrency control to optimize performance
- Implement comprehensive logging
- Test thoroughly with various scenarios
Azure Integration Hub - Logic Apps