← Back to Guides

Modular Logic Apps — Linking and Composing Workflows

AdvancedAzure Logic Apps2026-03-14

Why Go Modular?

A single monolithic Logic App that handles everything — validation, transformation, routing, error handling, notifications — quickly becomes difficult to maintain, test, and scale. Modular design splits that monolith into focused, reusable workflows that communicate with each other.

Microsoft Reference: Call, trigger, or nest logic apps

The Case for Modular Logic Apps

Concern Monolithic Workflow Modular Workflows
Maintainability Single large workflow is hard to navigate Each workflow has a clear, focused purpose
Reusability Logic is duplicated across workflows Shared child workflows eliminate duplication
Scaling Entire workflow shares one concurrency limit Each module scales independently
Error isolation One failure can cascade through everything Failures are contained within their module
Team ownership One team must understand the whole flow Teams own specific modules
Deployment Redeploy everything for a small change Deploy only the changed module
Testing Must test the entire end-to-end flow Test each module in isolation
Action limits Risk hitting the 500-action limit per workflow Each module stays well within limits

When Modular Design is the Right Choice

Modular Logic Apps are the recommended approach when:

  • Your workflow exceeds 50+ actions or is becoming difficult to follow in the designer
  • You have shared logic (e.g. address validation, notification sending) used by multiple workflows
  • Different parts of the workflow need different scaling or concurrency settings
  • Multiple teams contribute to the same integration
  • You need independent deployment of workflow components
  • You are approaching the 500-action limit per workflow (Consumption) or want to keep workflows manageable

For simple, self-contained workflows with fewer than 20 actions, a single Logic App is perfectly fine — do not over-engineer.

Linking Patterns

There are several ways to connect Logic Apps together. Each pattern has different characteristics for coupling, latency, and reliability.

Pattern 1 — Child Workflow (Synchronous, Tightly Coupled)

The native Workflow action calls a child Logic App directly and waits for its response. This is the simplest and most common pattern.

{
  "Call_Validation_Workflow": {
    "type": "Workflow",
    "inputs": {
      "host": {
        "triggerName": "manual",
        "workflow": {
          "id": "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Logic/workflows/la-validate-order-prod-uksouth"
        }
      },
      "body": {
        "orderId": "@triggerBody()?['orderId']",
        "items": "@triggerBody()?['lineItems']",
        "customer": "@triggerBody()?['customer']"
      }
    }
  }
}

The child workflow must have a Request trigger (manual) and should return a Response action with the result.

Child workflow (Request trigger):

{
  "triggers": {
    "manual": {
      "type": "Request",
      "kind": "Http",
      "inputs": {
        "schema": {
          "type": "object",
          "properties": {
            "orderId": { "type": "string" },
            "items": { "type": "array" },
            "customer": { "type": "object" }
          },
          "required": ["orderId", "items"]
        }
      }
    }
  }
}

Child workflow (Response action):

{
  "Return_Validation_Result": {
    "type": "Response",
    "inputs": {
      "statusCode": 200,
      "body": {
        "isValid": true,
        "orderId": "@triggerBody()?['orderId']",
        "validatedAt": "@utcNow()"
      }
    }
  }
}

Characteristics:

Aspect Detail
Coupling Tight — parent references child by resource ID
Latency Low — direct invocation within Azure fabric
Response Synchronous — parent waits for child to complete
Error handling Child failures propagate to parent as failed actions
Authentication Automatic — uses Azure Resource Manager identity
Cost Single connector invocation (no HTTP metering)

Microsoft Reference: Call nested or child logic apps

Pattern 2 — HTTP Call (Synchronous, Loosely Coupled)

Call another Logic App via its HTTP endpoint. This is useful when the child is in a different subscription, tenant, or when you want URL-based decoupling.

{
  "Call_Transform_Service": {
    "type": "Http",
    "inputs": {
      "method": "POST",
      "uri": "https://prod-01.uksouth.logic.azure.com:443/workflows/{workflowId}/triggers/manual/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig={sig}",
      "body": {
        "sourceFormat": "CSV",
        "targetFormat": "JSON",
        "payload": "@body('Get_File_Content')"
      },
      "retryPolicy": {
        "type": "exponential",
        "count": 3,
        "interval": "PT10S",
        "maximumInterval": "PT1H"
      }
    }
  }
}

Tip: Store the callback URL in an App Configuration or Key Vault reference rather than hardcoding it. This allows you to swap child endpoints across environments without redeploying the parent.

Characteristics:

Aspect Detail
Coupling Loose — parent only knows a URL
Latency Slightly higher — full HTTP round-trip
Response Synchronous by default; use 202 pattern for long-running
Error handling HTTP status codes; configure retry policies
Authentication SAS token in URL, or use Managed Identity with OAuth
Cost HTTP connector action cost applies

Pattern 3 — Service Bus Decoupling (Asynchronous, Loosely Coupled)

Use Azure Service Bus as a message broker between Logic Apps for fully asynchronous, reliable communication.

Producer workflow (sends message):

{
  "Send_Order_To_Queue": {
    "type": "ApiConnection",
    "inputs": {
      "host": {
        "connection": {
          "name": "@parameters('$connections')['servicebus']['connectionId']"
        }
      },
      "method": "post",
      "path": "/@{encodeURIComponent('order-processing')}/messages",
      "body": {
        "ContentData": "@{base64(triggerBody())}",
        "Properties": {
          "orderType": "@triggerBody()?['type']",
          "priority": "@triggerBody()?['priority']",
          "correlationId": "@triggerBody()?['orderId']"
        }
      }
    }
  }
}

Consumer workflow (receives and processes):

{
  "triggers": {
    "When_a_message_is_received": {
      "type": "ApiConnection",
      "inputs": {
        "host": {
          "connection": {
            "name": "@parameters('$connections')['servicebus']['connectionId']"
          }
        },
        "method": "get",
        "path": "/@{encodeURIComponent('order-processing')}/messages/head",
        "queries": {
          "queueType": "Main"
        }
      },
      "recurrence": {
        "frequency": "Second",
        "interval": 30
      }
    }
  }
}

Characteristics:

Aspect Detail
Coupling Very loose — producer and consumer share only a queue name
Latency Variable — depends on polling interval
Response Asynchronous — producer does not wait for consumer
Error handling Dead-letter queues, message retry, sessions
Reliability Messages persist in the queue if consumer is unavailable
Cost Service Bus + connector action costs

Microsoft Reference: Service Bus connector

Pattern 4 — Event Grid (Event-Driven, Loosely Coupled)

Use Azure Event Grid for reactive, event-driven chaining. One Logic App publishes an event and one or more Logic Apps subscribe to it.

{
  "Publish_Order_Completed_Event": {
    "type": "Http",
    "inputs": {
      "method": "POST",
      "uri": "https://{topic-name}.uksouth-1.eventgrid.azure.net/api/events",
      "headers": {
        "aeg-sas-key": "@parameters('eventGridKey')",
        "Content-Type": "application/json"
      },
      "body": [
        {
          "id": "@guid()",
          "eventType": "Order.Completed",
          "subject": "/orders/@{triggerBody()?['orderId']}",
          "dataVersion": "1.0",
          "data": {
            "orderId": "@triggerBody()?['orderId']",
            "total": "@triggerBody()?['total']",
            "completedAt": "@utcNow()"
          }
        }
      ]
    }
  }
}

Multiple subscriber Logic Apps can react to the same event — for example, one sends a confirmation email, another updates inventory, and a third feeds analytics.

Characteristics:

Aspect Detail
Coupling Very loose — fan-out to multiple subscribers
Latency Near real-time push delivery
Response Asynchronous — publisher does not wait
Scaling Event Grid handles millions of events per second
Use case One event triggers multiple independent downstream workflows

Microsoft Reference: Event Grid trigger for Logic Apps

Choosing the Right Pattern

Scenario Recommended Pattern
Shared validation or transformation logic Child Workflow
Need the child result before continuing Child Workflow or HTTP
Cross-subscription or cross-tenant calls HTTP
Producer should not wait for consumer Service Bus
Guaranteed delivery even if consumer is down Service Bus
One event should trigger multiple workflows Event Grid
Content-based routing to different processors Service Bus (Topics + Subscriptions)
Simple reusable utility (e.g. send email) Child Workflow

Modular Architecture Example

Here is a real-world order processing system broken into modular Logic Apps:

                        ┌──────────────────┐
                        │  la-order-api    │
                        │  (Entry Point)   │
                        └───────┬──────────┘
                                │
                    ┌───────────┼───────────┐
                    ▼           ▼           ▼
          ┌─────────────┐ ┌──────────┐ ┌──────────────┐
          │ la-validate  │ │ la-enrich│ │ la-check-    │
          │ -order       │ │ -customer│ │  inventory   │
          │ (Child)      │ │ (Child)  │ │ (Child)      │
          └──────┬───────┘ └────┬─────┘ └──────┬───────┘
                 │              │               │
                 └──────────────┼───────────────┘
                                ▼
                     ┌───────────────────┐
                     │  Service Bus      │
                     │  order-processing │
                     └────────┬──────────┘
                              │
                 ┌────────────┼────────────┐
                 ▼            ▼            ▼
        ┌──────────────┐ ┌──────────┐ ┌───────────────┐
        │ la-process   │ │ la-send  │ │ la-update     │
        │ -payment     │ │ -confirm │ │ -analytics    │
        │ (Consumer)   │ │ (Consumer│ │ (Consumer)    │
        └──────────────┘ └──────────┘ └───────────────┘

How it works:

  1. la-order-api receives the incoming order via HTTP trigger
  2. It calls three child workflows in parallel (validate, enrich, check inventory) using the Workflow action
  3. If all three succeed, it publishes a message to a Service Bus queue
  4. Three independent consumer Logic Apps pick up the message and handle payment, confirmation email, and analytics — each at their own pace

This design means the validation logic can be reused by other entry points (e.g. a batch import workflow), payment processing can scale independently, and a failure in email sending does not block payment processing.

Consumption vs Standard — Modular Approaches

The hosting model affects how you implement modular designs.

Consumption (Multi-Tenant)

In Consumption, each Logic App is a separate Azure resource. Modular design means creating multiple Logic App resources:

Resource Group: rg-orders-prod-uksouth
├── la-order-api-prod-uksouth          (parent)
├── la-validate-order-prod-uksouth     (child)
├── la-enrich-customer-prod-uksouth    (child)
├── la-check-inventory-prod-uksouth    (child)
└── la-process-payment-prod-uksouth    (consumer)
  • Use the Workflow action type to call child Logic Apps by resource ID
  • Each Logic App has its own trigger, scaling, and run history
  • Deploy each independently via ARM/Bicep
  • Connector connections are shared within the resource group

Standard (Single-Tenant)

In Standard, a single Logic App resource can contain multiple workflows. This is a natural fit for modular design:

Logic App: la-orders-prod-uksouth
├── workflows/
│   ├── order-api/           (stateful - entry point)
│   ├── validate-order/      (stateless - fast validation)
│   ├── enrich-customer/     (stateless - lookup + transform)
│   ├── check-inventory/     (stateful - external API call)
│   └── process-payment/     (stateful - durable processing)
  • Workflows within the same Logic App call each other directly using the Invoke Workflow action
  • Stateless workflows are ideal for lightweight child modules (lower latency, no run history overhead)
  • Stateful workflows are better for modules that need durable execution and run history
  • All workflows share the same App Service Plan, VNET integration, and deployment unit
  • Deploy the entire Logic App as one unit, but update individual workflow definitions independently

Standard workflow invocation:

{
  "Invoke_Validate_Order": {
    "type": "InvokeWorkflow",
    "inputs": {
      "host": {
        "workflow": {
          "id": "validate-order"
        }
      },
      "body": {
        "orderId": "@triggerBody()?['orderId']",
        "items": "@triggerBody()?['lineItems']"
      }
    }
  }
}

Microsoft Reference: Create Standard Logic App workflows

Passing Data Between Modules

Request/Response Body

The most common pattern — pass data in the request body and receive results in the response body:

{
  "Call_Enrichment": {
    "type": "Workflow",
    "inputs": {
      "body": {
        "customerId": "@triggerBody()?['customerId']"
      }
    }
  },
  "Use_Enrichment_Result": {
    "type": "Compose",
    "inputs": {
      "customerName": "@body('Call_Enrichment')?['name']",
      "creditLimit": "@body('Call_Enrichment')?['creditLimit']"
    },
    "runAfter": {
      "Call_Enrichment": ["Succeeded"]
    }
  }
}

Correlation with Message Properties

When using Service Bus or Event Grid (asynchronous patterns), use correlation IDs to link related messages across workflows:

{
  "Send_To_Queue": {
    "inputs": {
      "body": {
        "Properties": {
          "correlationId": "@triggerBody()?['orderId']",
          "sourceWorkflow": "@workflow().name",
          "timestamp": "@utcNow()"
        }
      }
    }
  }
}

The consumer can then use the correlationId to track the message back to the originating workflow, query related data, and maintain an audit trail.

Shared State via External Storage

For data too large to pass in message bodies (Logic Apps has a 100 MB message size limit), use external storage:

{
  "Store_Payload_In_Blob": {
    "type": "ApiConnection",
    "inputs": {
      "path": "/v2/datasets/default/files",
      "body": "@triggerBody()?['largePayload']",
      "queries": {
        "folderPath": "/workflow-data/@{triggerBody()?['correlationId']}",
        "name": "payload.json"
      }
    }
  },
  "Call_Processor_With_Reference": {
    "type": "Workflow",
    "inputs": {
      "body": {
        "correlationId": "@triggerBody()?['correlationId']",
        "blobPath": "/workflow-data/@{triggerBody()?['correlationId']}/payload.json"
      }
    },
    "runAfter": {
      "Store_Payload_In_Blob": ["Succeeded"]
    }
  }
}

This is the Claim Check pattern — pass a reference instead of the full payload.

Microsoft Reference: Handle large messages in Logic Apps

Error Handling Across Modules

Child Workflow Failures

When a child workflow fails, the parent receives a failed action. Use runAfter with status filters to handle this:

{
  "Call_Validation": {
    "type": "Workflow",
    "inputs": { }
  },
  "Handle_Validation_Success": {
    "type": "Compose",
    "runAfter": {
      "Call_Validation": ["Succeeded"]
    }
  },
  "Handle_Validation_Failure": {
    "type": "Compose",
    "inputs": {
      "error": "Validation workflow failed",
      "details": "@body('Call_Validation')",
      "failedAt": "@utcNow()"
    },
    "runAfter": {
      "Call_Validation": ["Failed", "TimedOut"]
    }
  }
}

Returning Structured Errors from Child Workflows

Design child workflows to return meaningful error responses rather than just failing:

{
  "Scope_Validation_Logic": {
    "type": "Scope",
    "actions": {
      "Validate_Fields": { },
      "Check_Business_Rules": { }
    }
  },
  "Return_Success": {
    "type": "Response",
    "inputs": {
      "statusCode": 200,
      "body": { "isValid": true }
    },
    "runAfter": {
      "Scope_Validation_Logic": ["Succeeded"]
    }
  },
  "Return_Error": {
    "type": "Response",
    "inputs": {
      "statusCode": 400,
      "body": {
        "isValid": false,
        "errorCode": "VALIDATION_FAILED",
        "message": "@result('Scope_Validation_Logic')?[0]?['error']?['message']"
      }
    },
    "runAfter": {
      "Scope_Validation_Logic": ["Failed"]
    }
  }
}

The parent can then check the status code and act accordingly, rather than relying solely on action failure states.

Dead-Letter Handling for Async Patterns

When using Service Bus, configure dead-letter handling so failed messages are not lost:

{
  "triggers": {
    "When_deadletter_message_received": {
      "type": "ApiConnection",
      "inputs": {
        "path": "/@{encodeURIComponent('order-processing')}/messages/head",
        "queries": {
          "queueType": "DeadLetter"
        }
      },
      "recurrence": {
        "frequency": "Minute",
        "interval": 5
      }
    }
  }
}

Create a dedicated Logic App that monitors the dead-letter queue, logs the failures, and optionally retries or alerts.

Deployment Considerations

Naming Convention

Follow a consistent naming pattern so modular workflows are easy to identify:

la-{domain}-{function}-{environment}-{region}

Examples:
la-orders-validate-prod-uksouth       (child - validation)
la-orders-enrich-prod-uksouth         (child - enrichment)
la-orders-api-prod-uksouth            (parent - entry point)
la-orders-dlq-handler-prod-uksouth    (utility - dead-letter)

Deploying Modular Logic Apps with Bicep

Deploy related modular Logic Apps together using a Bicep module:

// modules/logic-app-child.bicep
param logicAppName string
param location string
param definition object
param connections object = {}

resource logicApp 'Microsoft.Logic/workflows@2019-05-01' = {
  name: logicAppName
  location: location
  properties: {
    state: 'Enabled'
    definition: definition
    parameters: {
      '$connections': {
        value: connections
      }
    }
  }
}

output callbackUrl string = logicApp.listCallbackUrl().value
output resourceId string = logicApp.id
// main.bicep
param environment string
param location string = resourceGroup().location

module validateOrder 'modules/logic-app-child.bicep' = {
  name: 'deploy-validate-order'
  params: {
    logicAppName: 'la-orders-validate-${environment}-uksouth'
    location: location
    definition: loadJsonContent('workflows/validate-order.json')
  }
}

module enrichCustomer 'modules/logic-app-child.bicep' = {
  name: 'deploy-enrich-customer'
  params: {
    logicAppName: 'la-orders-enrich-${environment}-uksouth'
    location: location
    definition: loadJsonContent('workflows/enrich-customer.json')
  }
}

module orderApi 'modules/logic-app-child.bicep' = {
  name: 'deploy-order-api'
  params: {
    logicAppName: 'la-orders-api-${environment}-uksouth'
    location: location
    definition: loadJsonContent('workflows/order-api.json')
  }
  dependsOn: [
    validateOrder
    enrichCustomer
  ]
}

Tip: Deploy child workflows before parent workflows so the parent can reference valid resource IDs. Use dependsOn in Bicep to enforce this ordering.

Environment-Specific Configuration

Use Logic App parameters to manage environment differences without changing workflow definitions:

{
  "definition": {
    "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
    "parameters": {
      "childWorkflowId": {
        "type": "String",
        "defaultValue": ""
      },
      "serviceBusConnection": {
        "type": "String",
        "defaultValue": ""
      }
    },
    "actions": {
      "Call_Child": {
        "type": "Workflow",
        "inputs": {
          "host": {
            "workflow": {
              "id": "@parameters('childWorkflowId')"
            }
          }
        }
      }
    }
  }
}

Set childWorkflowId per environment in your deployment pipeline so the same workflow definition works in dev, test, and production.

Best Practices

  1. Start simple — Begin with a monolithic workflow and refactor into modules when complexity justifies it
  2. Define clear contracts — Document the request and response schema for each child workflow
  3. Use child workflows for synchronous reuse — When you need the result before continuing
  4. Use Service Bus for async decoupling — When producer and consumer should operate independently
  5. Keep modules focused — Each workflow should do one thing well (single responsibility)
  6. Return structured errors — Child workflows should return error codes and messages, not just fail
  7. Add correlation IDs — Thread a correlation ID through all linked workflows for end-to-end tracing
  8. Use Application Insights — Enable tracked properties with the correlation ID for cross-workflow monitoring
  9. Deploy children first — Ensure child workflows exist before deploying parents that reference them
  10. Parameterise references — Use Logic App parameters for child workflow IDs and URLs so environments stay independent
  11. Prefer Standard for complex modular systems — Multiple workflows in one Logic App simplify networking, deployment, and cost management
  12. Monitor the full chain — Set up alerts on each module, not just the entry point

Official Microsoft Resources