← Back to ArticlesLogic Apps

Enterprise Workflow Automation with Logic Apps

Comprehensive guide to building production-grade workflows with Azure Logic Apps using error handling, parallel execution, and enterprise integration patterns

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:

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

PracticeWhyImplementation
Use ScopesGroup actions for error handlingTry-Catch pattern
Retry PoliciesHandle transient failuresExponential backoff
Concurrency ControlOptimize performanceBalance with API limits
LoggingDebug and monitorAdd compose actions
Managed IdentitySecure accessNo credentials in code

Conclusion

Enterprise Logic Apps require:

Key takeaways:

  1. Always use Scopes for error handling
  2. Set retry policies on all external calls
  3. Use concurrency control to optimize performance
  4. Implement comprehensive logging
  5. Test thoroughly with various scenarios

Azure Integration Hub - Logic Apps