CI/CD for Logic Apps Standard with Azure DevOps
In this tutorial, you'll build a complete CI/CD pipeline for Logic Apps Standard using Azure DevOps. You'll learn how to structure your project, automate builds and deployments, manage environment-specific configuration, and achieve zero-downtime deployments with slots.
Prerequisites
- Azure subscription with Logic Apps Standard resource
- Azure DevOps organization and project
- VS Code with Azure Logic Apps (Standard) extension
- Azure CLI and Bicep CLI installed
- Basic familiarity with YAML pipelines
Pipeline Architecture
┌─────────────────────────────────────────────────────────────────────────┐
│ Azure DevOps Pipeline │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────┐ ┌───────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ Trigger │───▶│ Build │───▶│ Deploy to │───▶│ Swap Slot │ │
│ │ (main) │ │ & Test │ │ Staging │ │ to Prod │ │
│ └───────────┘ └───────────┘ └────────────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────┐ │
│ │ Publish Artifact │ │
│ │ (zip package) │ │
│ └───────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────────────┤
│ Environments: DEV ──────▶ STAGING ──────▶ PRODUCTION │
│ (auto) (auto) (manual approval) │
└─────────────────────────────────────────────────────────────────────────┘
Logic Apps Standard Project Structure
A Logic Apps Standard project follows a specific folder structure that maps directly to how workflows are deployed:
my-logic-app/
├── host.json # Runtime configuration
├── connections.json # API connections and managed connectors
├── parameters.json # Workflow parameters
├── local.settings.json # Local dev settings (not deployed)
├── Artifacts/
│ └── Maps/ # XSLT maps, Liquid templates
├── workflow-order-processing/
│ └── workflow.json # Workflow definition
├── workflow-notification/
│ └── workflow.json # Another workflow
└── .vscode/
├── extensions.json
├── launch.json
└── settings.json
host.json
The host.json file configures the Logic Apps runtime:
{
"version": "2.0",
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle.Workflows",
"version": "[1.*, 2.0.0)"
},
"extensions": {
"workflow": {
"settings": {
"Runtime.FlowRunRetryableActionJobCallback.ActionJobExecutionTimeout": "00:10:00"
}
}
}
}
connections.json
Defines managed API connections and service provider connections:
{
"managedApiConnections": {
"office365": {
"api": {
"id": "/subscriptions/{sub}/providers/Microsoft.Web/locations/{region}/managedApis/office365"
},
"connection": {
"id": "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/connections/office365"
},
"connectionRuntimeUrl": "https://{connection-runtime-url}",
"authentication": {
"type": "ManagedServiceIdentity"
}
}
},
"serviceProviderConnections": {
"serviceBus": {
"parameterValues": {
"connectionString": "@appsetting('ServiceBus_ConnectionString')"
},
"serviceProvider": {
"id": "/serviceProviders/serviceBus"
}
}
}
}
workflow.json
Each workflow lives in its own folder with a workflow.json definition:
{
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"actions": {
"Send_message": {
"type": "ServiceProvider",
"inputs": {
"parameters": {
"entityName": "orders",
"message": {
"contentData": "@triggerBody()"
}
},
"serviceProviderConfiguration": {
"connectionName": "serviceBus",
"operationId": "sendMessage",
"serviceProviderId": "/serviceProviders/serviceBus"
}
}
}
},
"triggers": {
"When_a_HTTP_request_is_received": {
"type": "Request",
"kind": "Http",
"inputs": {
"method": "POST"
}
}
},
"contentVersion": "1.0.0.0"
},
"kind": "Stateful"
}

Local Development with VS Code Extension
Setting Up Your Environment
- Install the Azure Logic Apps (Standard) extension for VS Code
- Install the Azurite extension for local storage emulation
- Install .NET Core SDK 6.0 and Azure Functions Core Tools v4
Creating a New Project
- Open the Command Palette (
Cmd+Shift+P) - Select Azure Logic Apps: Create New Project
- Choose your workspace folder
- Select Stateful Workflow or Stateless Workflow
Running Locally
Configure local.settings.json for local development:
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "node",
"ServiceBus_ConnectionString": "Endpoint=sb://dev-servicebus.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=...",
"WORKFLOWS_SUBSCRIPTION_ID": "your-subscription-id"
}
}
Start the project:
func host start
The workflow designer opens in VS Code, allowing you to visually edit and test workflows.

Bicep Template for Logic App Infrastructure
main.bicep
@description('Environment name')
param environmentName string
@description('Azure region')
param location string = resourceGroup().location
@description('Logic App name')
param logicAppName string = 'la-${environmentName}-orderprocessing'
@description('App Service Plan SKU')
param skuName string = 'WS1'
var appServicePlanName = 'asp-${environmentName}-logicapps'
var storageAccountName = 'st${environmentName}la${uniqueString(resourceGroup().id)}'
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
}
resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = {
name: appServicePlanName
location: location
sku: {
name: skuName
tier: 'WorkflowStandard'
}
kind: 'elastic'
properties: {
elasticScaleEnabled: true
maximumElasticWorkerCount: 20
}
}
resource logicApp 'Microsoft.Web/sites@2023-01-01' = {
name: logicAppName
location: location
kind: 'functionapp,workflowapp'
identity: {
type: 'SystemAssigned'
}
properties: {
serverFarmId: appServicePlan.id
siteConfig: {
netFrameworkVersion: 'v6.0'
appSettings: [
{
name: 'AzureWebJobsStorage'
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value};EndpointSuffix=core.windows.net'
}
{
name: 'FUNCTIONS_EXTENSION_VERSION'
value: '~4'
}
{
name: 'FUNCTIONS_WORKER_RUNTIME'
value: 'node'
}
{
name: 'WEBSITE_NODE_DEFAULT_VERSION'
value: '~18'
}
]
}
}
}
// Deployment slot for zero-downtime deployments
resource stagingSlot 'Microsoft.Web/sites/slots@2023-01-01' = {
parent: logicApp
name: 'staging'
location: location
kind: 'functionapp,workflowapp'
identity: {
type: 'SystemAssigned'
}
properties: {
serverFarmId: appServicePlan.id
siteConfig: {
netFrameworkVersion: 'v6.0'
appSettings: [
{
name: 'AzureWebJobsStorage'
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value};EndpointSuffix=core.windows.net'
}
{
name: 'FUNCTIONS_EXTENSION_VERSION'
value: '~4'
}
{
name: 'FUNCTIONS_WORKER_RUNTIME'
value: 'node'
}
]
}
}
}
output logicAppName string = logicApp.name
output logicAppIdentityPrincipalId string = logicApp.identity.principalId
output stagingSlotName string = stagingSlot.name

Environment-Specific Configuration
App Settings per Environment
Use app settings to externalize environment-specific values. Reference them in connections.json with @appsetting('KEY'):
| Setting | Dev | Staging | Production |
|---|---|---|---|
ServiceBus_ConnectionString | Dev namespace | Staging namespace | Prod namespace |
SQL_ConnectionString | Dev database | Staging database | Prod database |
API_BaseUrl | https://api-dev.example.com | https://api-staging.example.com | https://api.example.com |
WORKFLOWS_SUBSCRIPTION_ID | Dev subscription | Staging subscription | Prod subscription |
Parameter Files for Dev/Staging/Prod
Create environment-specific parameter files for Bicep deployments:
parameters/dev.bicepparam
using '../main.bicep'
param environmentName = 'dev'
param location = 'eastus'
param skuName = 'WS1'
parameters/staging.bicepparam
using '../main.bicep'
param environmentName = 'staging'
param location = 'eastus'
param skuName = 'WS1'
parameters/prod.bicepparam
using '../main.bicep'
param environmentName = 'prod'
param location = 'eastus'
param skuName = 'WS2'
App Settings Override Files
Create environment-specific app settings for pipeline deployment:
config/appsettings.dev.json
{
"ServiceBus_ConnectionString": "$(ServiceBus-Dev-ConnectionString)",
"SQL_ConnectionString": "$(SQL-Dev-ConnectionString)",
"API_BaseUrl": "https://api-dev.example.com",
"APPINSIGHTS_INSTRUMENTATIONKEY": "$(AppInsights-Dev-Key)"
}
config/appsettings.prod.json
{
"ServiceBus_ConnectionString": "$(ServiceBus-Prod-ConnectionString)",
"SQL_ConnectionString": "$(SQL-Prod-ConnectionString)",
"API_BaseUrl": "https://api.example.com",
"APPINSIGHTS_INSTRUMENTATIONKEY": "$(AppInsights-Prod-Key)"
}

Azure DevOps Pipeline YAML (Build + Deploy)
azure-pipelines.yml
trigger:
branches:
include:
- main
paths:
include:
- src/logic-app/**
pool:
vmImage: 'ubuntu-latest'
variables:
- group: 'logic-app-common'
- name: workingDirectory
value: 'src/logic-app'
- name: artifactName
value: 'logic-app-package'
stages:
# ─────────────────────────────────────────────
# BUILD STAGE
# ─────────────────────────────────────────────
- stage: Build
displayName: 'Build Logic App'
jobs:
- job: BuildJob
displayName: 'Build and Package'
steps:
- task: UseDotNet@2
displayName: 'Install .NET SDK'
inputs:
packageType: 'sdk'
version: '6.x'
- task: CopyFiles@2
displayName: 'Copy Logic App files'
inputs:
SourceFolder: '$(workingDirectory)'
Contents: |
host.json
connections.json
parameters.json
Artifacts/**
workflow-*/**
TargetFolder: '$(Build.ArtifactStagingDirectory)/app'
- task: ArchiveFiles@2
displayName: 'Create deployment package'
inputs:
rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/app'
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/$(artifactName).zip'
- task: PublishBuildArtifacts@1
displayName: 'Publish artifact'
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)/$(artifactName).zip'
ArtifactName: '$(artifactName)'
# ─────────────────────────────────────────────
# DEPLOY TO DEV
# ─────────────────────────────────────────────
- stage: DeployDev
displayName: 'Deploy to Dev'
dependsOn: Build
variables:
- group: 'logic-app-dev'
jobs:
- deployment: DeployDev
displayName: 'Deploy to Dev Environment'
environment: 'logic-app-dev'
strategy:
runOnce:
deploy:
steps:
- task: AzureAppServiceSettings@1
displayName: 'Configure app settings'
inputs:
azureSubscription: '$(azureServiceConnection)'
appName: 'la-dev-orderprocessing'
appSettings: |
[
{ "name": "ServiceBus_ConnectionString", "value": "$(ServiceBus-Dev-ConnectionString)", "slotSetting": false },
{ "name": "API_BaseUrl", "value": "https://api-dev.example.com", "slotSetting": false }
]
- task: AzureFunctionApp@2
displayName: 'Deploy Logic App'
inputs:
azureSubscription: '$(azureServiceConnection)'
appType: 'functionApp'
appName: 'la-dev-orderprocessing'
package: '$(Pipeline.Workspace)/$(artifactName)/$(artifactName).zip'
deploymentMethod: 'zipDeploy'
# ─────────────────────────────────────────────
# DEPLOY TO STAGING (with slot)
# ─────────────────────────────────────────────
- stage: DeployStaging
displayName: 'Deploy to Staging'
dependsOn: DeployDev
variables:
- group: 'logic-app-staging'
jobs:
- deployment: DeployStaging
displayName: 'Deploy to Staging Slot'
environment: 'logic-app-staging'
strategy:
runOnce:
deploy:
steps:
- task: AzureFunctionApp@2
displayName: 'Deploy to staging slot'
inputs:
azureSubscription: '$(azureServiceConnection)'
appType: 'functionApp'
appName: 'la-staging-orderprocessing'
package: '$(Pipeline.Workspace)/$(artifactName)/$(artifactName).zip'
deploymentMethod: 'zipDeploy'
deployToSlotOrASE: true
slotName: 'staging'
- task: AzureAppServiceManage@0
displayName: 'Swap staging to production'
inputs:
azureSubscription: '$(azureServiceConnection)'
Action: 'Swap Slots'
WebAppName: 'la-staging-orderprocessing'
ResourceGroupName: '$(resourceGroupName)'
SourceSlot: 'staging'
# ─────────────────────────────────────────────
# DEPLOY TO PRODUCTION (with approval + slot)
# ─────────────────────────────────────────────
- stage: DeployProd
displayName: 'Deploy to Production'
dependsOn: DeployStaging
variables:
- group: 'logic-app-prod'
jobs:
- deployment: DeployProd
displayName: 'Deploy to Production Slot'
environment: 'logic-app-prod' # Configure approval gate on this environment
strategy:
runOnce:
deploy:
steps:
- task: AzureFunctionApp@2
displayName: 'Deploy to staging slot'
inputs:
azureSubscription: '$(azureServiceConnection)'
appType: 'functionApp'
appName: 'la-prod-orderprocessing'
package: '$(Pipeline.Workspace)/$(artifactName)/$(artifactName).zip'
deploymentMethod: 'zipDeploy'
deployToSlotOrASE: true
slotName: 'staging'
- task: AzureCLI@2
displayName: 'Warm up staging slot'
inputs:
azureSubscription: '$(azureServiceConnection)'
scriptType: 'bash'
inlineScript: |
echo "Warming up staging slot..."
curl -s -o /dev/null -w "%{http_code}" https://la-prod-orderprocessing-staging.azurewebsites.net/
sleep 30
- task: AzureAppServiceManage@0
displayName: 'Swap staging to production'
inputs:
azureSubscription: '$(azureServiceConnection)'
Action: 'Swap Slots'
WebAppName: 'la-prod-orderprocessing'
ResourceGroupName: '$(resourceGroupName)'
SourceSlot: 'staging'

Step-by-Step: Set Up a Complete CI/CD Pipeline
Step 1: Organize Your Repository
Structure your repository to separate infrastructure from application code:
repo-root/
├── infra/
│ ├── main.bicep
│ └── parameters/
│ ├── dev.bicepparam
│ ├── staging.bicepparam
│ └── prod.bicepparam
├── src/
│ └── logic-app/
│ ├── host.json
│ ├── connections.json
│ ├── parameters.json
│ ├── workflow-order-processing/
│ │ └── workflow.json
│ └── workflow-notification/
│ └── workflow.json
├── config/
│ ├── appsettings.dev.json
│ ├── appsettings.staging.json
│ └── appsettings.prod.json
├── azure-pipelines.yml
└── README.md
Step 2: Create Azure DevOps Service Connection
- Navigate to Project Settings > Service connections
- Click New service connection > Azure Resource Manager
- Select Service principal (automatic)
- Choose your subscription and resource group
- Name it (e.g.,
azure-logic-app-connection)

Step 3: Configure Variable Groups
Create variable groups for each environment in Pipelines > Library:
logic-app-common— shared variables (service connection name, artifact name)logic-app-dev— dev connection strings and keyslogic-app-staging— staging connection strings and keyslogic-app-prod— production connection strings and keys
Link Azure Key Vault for secrets:
- Toggle Link secrets from an Azure key vault
- Select your Key Vault
- Add the secrets you need (connection strings, keys)
Step 4: Deploy Infrastructure with Bicep
Run the Bicep deployment for each environment:
# Deploy dev environment
az deployment group create \
--resource-group rg-dev-logicapps \
--template-file infra/main.bicep \
--parameters infra/parameters/dev.bicepparam
# Deploy staging environment
az deployment group create \
--resource-group rg-staging-logicapps \
--template-file infra/main.bicep \
--parameters infra/parameters/staging.bicepparam
# Deploy production environment
az deployment group create \
--resource-group rg-prod-logicapps \
--template-file infra/main.bicep \
--parameters infra/parameters/prod.bicepparam
Step 5: Configure Environment Approvals
- Go to Pipelines > Environments
- Select
logic-app-prod - Click the ⋮ menu > Approvals and checks
- Add Approvals — specify required approvers
- Optionally add Business Hours check to prevent off-hours deployments
Step 6: Create and Run the Pipeline
- Go to Pipelines > New Pipeline
- Select your repository source
- Choose Existing Azure Pipelines YAML file
- Select
azure-pipelines.yml - Click Run

Step 7: Verify Deployment
After the pipeline completes:
# Check Logic App status
az logicapp show --name la-dev-orderprocessing --resource-group rg-dev-logicapps --query "state"
# List workflows
az rest --method get \
--uri "https://management.azure.com/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/sites/la-dev-orderprocessing/workflows?api-version=2023-01-01"
Deployment Slots for Zero-Downtime
Deployment slots allow you to deploy to a staging environment and swap it into production without downtime.
How Slot Swapping Works
- Deploy new code to the
stagingslot - Warm up the staging slot (pre-load workflows, establish connections)
- Validate the staging slot is healthy
- Swap staging into production — traffic switches instantly
- Rollback if needed by swapping back
Slot-Specific Settings
Mark settings as "slot settings" to keep them tied to a specific slot:
az webapp config appsettings set \
--name la-prod-orderprocessing \
--resource-group rg-prod-logicapps \
--slot-settings "SLOT_NAME=production"
Health Check Configuration
Configure a health check endpoint so Azure can verify the slot is ready before swapping:
az webapp config set \
--name la-prod-orderprocessing \
--resource-group rg-prod-logicapps \
--generic-configurations '{"healthCheckPath": "/api/health"}'
Rollback Strategy
If issues are detected after a swap:
# Swap back immediately
az webapp deployment slot swap \
--name la-prod-orderprocessing \
--resource-group rg-prod-logicapps \
--slot staging \
--target-slot production

Best Practices
- Never commit
local.settings.json— add it to.gitignore - Use Managed Identity for connections instead of connection strings where possible
- Parameterize everything — no hardcoded environment values in workflow definitions
- Version your connections — track
connections.jsonin source control with@appsetting()references - Test workflows locally before pushing to the pipeline
- Use branch policies — require PR reviews and successful builds before merging to main
- Monitor deployments — integrate Application Insights and set up alerts for failed runs
Troubleshooting
| Issue | Solution |
|---|---|
| Workflows not appearing after deploy | Ensure folder structure is correct; each workflow needs its own folder with workflow.json |
| Connection auth failures | Verify Managed Identity has correct RBAC roles on target resources |
| Slot swap fails | Check health check endpoint returns 200; review staging slot logs |
| Pipeline timeout | Increase timeout on deployment tasks; check if Logic App is stuck starting |
Summary
You've built a complete CI/CD pipeline for Logic Apps Standard that includes:
- ✅ Structured project with proper separation of concerns
- ✅ Infrastructure as Code with Bicep
- ✅ Multi-stage pipeline with dev, staging, and production
- ✅ Environment-specific configuration via variable groups and Key Vault
- ✅ Zero-downtime deployments using slot swapping
- ✅ Manual approval gates for production
← Previous: Stateful vs Stateless Workflows | Next: VNet & Private Endpoints →