Logic Apps — Integrating with Azure Functions for Custom Logic
Why Combine Logic Apps with Azure Functions?
Azure Logic Apps excels at orchestrating workflows with its visual designer and 400+ connectors. However, some operations require custom code — complex data transformations, business rule evaluation, cryptographic operations, or calling APIs with specific protocols. Azure Functions fills this gap perfectly.
Together they provide:
- Visual orchestration (Logic Apps) + custom code (Functions)
- No-code connectors for standard integrations + full .NET/Node.js for complex logic
- Built-in retry and error handling from Logic Apps wrapping your custom code
- Independent scaling — Functions scale separately from the workflow
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Logic App Workflow │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────────┐ ┌───────────────────────┐ │
│ │ Trigger │───▶│ Azure │───▶│ Continue Workflow │ │
│ │ (HTTP/ │ │ Function │ │ (Send email, update │ │
│ │ Queue) │ │ (Custom code)│ │ database, etc.) │ │
│ └──────────┘ └──────────────┘ └───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Returns JSON │ │
│ │ to workflow │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Step 1: Create an Azure Function for Logic Apps
HTTP-Triggered Function (C#)
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using System.Text.Json;
public class OrderValidator
{
private readonly ILogger<OrderValidator> _logger;
public OrderValidator(ILogger<OrderValidator> logger)
{
_logger = logger;
}
[Function("ValidateOrder")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req)
{
_logger.LogInformation("Validating order from Logic App");
var body = await JsonSerializer.DeserializeAsync<OrderRequest>(req.Body);
var result = new ValidationResult
{
IsValid = true,
Errors = new List<string>()
};
// Business rule: minimum order amount
if (body.TotalAmount < 10)
{
result.IsValid = false;
result.Errors.Add("Minimum order amount is $10");
}
// Business rule: max items per order
if (body.Items?.Count > 50)
{
result.IsValid = false;
result.Errors.Add("Maximum 50 items per order");
}
// Business rule: shipping address required for physical items
if (body.Items?.Any(i => i.IsPhysical) == true && string.IsNullOrEmpty(body.ShippingAddress))
{
result.IsValid = false;
result.Errors.Add("Shipping address required for physical items");
}
return new OkObjectResult(result);
}
}
public class OrderRequest
{
public string OrderId { get; set; }
public decimal TotalAmount { get; set; }
public List<OrderItem> Items { get; set; }
public string ShippingAddress { get; set; }
}
public class OrderItem
{
public string ProductId { get; set; }
public int Quantity { get; set; }
public bool IsPhysical { get; set; }
}
public class ValidationResult
{
public bool IsValid { get; set; }
public List<string> Errors { get; set; }
}
Step 2: Call the Function from Logic Apps
Using the Azure Functions Connector
In the Logic Apps designer:
- Add action → search "Azure Functions"
- Select your Function App
- Choose the function (e.g.,
ValidateOrder) - Pass the request body from previous steps
Workflow Definition (JSON)
{
"Call_Validation_Function": {
"type": "Function",
"inputs": {
"function": {
"id": "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/sites/{functionApp}/functions/ValidateOrder"
},
"body": {
"orderId": "@triggerBody()?['orderId']",
"totalAmount": "@triggerBody()?['totalAmount']",
"items": "@triggerBody()?['items']",
"shippingAddress": "@triggerBody()?['shippingAddress']"
}
},
"runAfter": {}
},
"Check_Validation_Result": {
"type": "If",
"expression": {
"equals": ["@body('Call_Validation_Function')?['isValid']", true]
},
"actions": {
"Process_Order": { "type": "Http", "inputs": { "...": "..." } }
},
"else": {
"actions": {
"Reject_Order": {
"type": "Response",
"inputs": {
"statusCode": 400,
"body": {
"error": "validation_failed",
"errors": "@body('Call_Validation_Function')?['errors']"
}
}
}
}
},
"runAfter": { "Call_Validation_Function": ["Succeeded"] }
}
}
Step 3: Data Transformation Function
A common use case — transforming data between formats that Logic Apps connectors can't handle natively:
[Function("TransformCsvToJson")]
public async Task<IActionResult> TransformCsv(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req)
{
using var reader = new StreamReader(req.Body);
var csvContent = await reader.ReadToEndAsync();
var lines = csvContent.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var headers = lines[0].Split(',').Select(h => h.Trim()).ToArray();
var records = new List<Dictionary<string, string>>();
for (int i = 1; i < lines.Length; i++)
{
var values = lines[i].Split(',');
var record = new Dictionary<string, string>();
for (int j = 0; j < headers.Length && j < values.Length; j++)
{
record[headers[j]] = values[j].Trim();
}
records.Add(record);
}
return new OkObjectResult(new { records, count = records.Count });
}
Step 4: Long-Running Function with Durable Functions
For operations that take longer than the Logic Apps HTTP timeout (120 seconds), use the async polling pattern:
[Function("StartLongProcess")]
public async Task<IActionResult> StartProcess(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
[DurableClient] DurableTaskClient client)
{
var input = await JsonSerializer.DeserializeAsync<ProcessRequest>(req.Body);
var instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
"LongRunningOrchestrator", input);
return client.CreateCheckStatusResponse(req, instanceId);
}
[Function("LongRunningOrchestrator")]
public async Task<ProcessResult> RunOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var input = context.GetInput<ProcessRequest>();
// Step 1: Validate
var validation = await context.CallActivityAsync<bool>("ValidateData", input);
// Step 2: Process (might take minutes)
var result = await context.CallActivityAsync<ProcessResult>("ProcessData", input);
// Step 3: Notify
await context.CallActivityAsync("SendNotification", result);
return result;
}
Logic Apps can poll the status URL returned by the Durable Function until completion.
Best Practices
| Practice | Why |
|---|---|
Use AuthorizationLevel.Function | Secure the function with a key |
| Return structured JSON | Logic Apps can parse and use in conditions |
| Keep functions focused | One function = one responsibility |
| Handle errors gracefully | Return error details in response body |
| Use Managed Identity | Avoid storing function keys in Logic Apps |
| Set appropriate timeouts | Match Logic Apps action timeout settings |
Summary
- Use Azure Functions when Logic Apps connectors can't handle your logic
- HTTP-triggered functions integrate seamlessly with the Logic Apps Azure Functions connector
- Return structured JSON so Logic Apps can branch on results
- Use Durable Functions for long-running operations
- Keep functions small and focused — let Logic Apps handle orchestration
Azure Integration Hub — Logic Apps