Standard Logic Apps Development
Why Standard Logic Apps?
Standard Logic Apps run on a single-tenant runtime based on the Azure Functions extensibility model. This provides significant advantages for enterprise development.
Microsoft Reference: Single-tenant Logic Apps overview
Key Advantages Over Consumption
Consumption
Multi-tenant — serverless, fully managed
Multiple workflows1 per resource
Local developmentNot supported
DebuggingPortal only
VNET integrationISE (deprecated)
DeploymentARM/Bicep
Stateless workflowsNot supported
Custom connectorsNot supported
Storage controlMicrosoft-managed
Version controlExport/import
Standard
Single-tenant — full development experience
Multiple workflowsUnlimited per app
Local developmentFull VS Code support
DebuggingVS Code breakpoints
VNET integrationNative VNET integration
DeploymentZIP deploy, containers
Stateless workflowsSupported (high performance)
Custom connectorsExtensibility framework
Storage controlYour storage account
Version controlFull Git integration
Development Environment Setup
Prerequisites
- Visual Studio Code — Download
- Azure Logic Apps (Standard) extension — Install from VS Code marketplace
- Azure Functions Core Tools v4 —
npm install -g azure-functions-core-tools@4 - Azurite — Local Azure Storage emulator (included in VS Code Azure extensions)
- .NET 6.0 SDK — Required for the Logic Apps runtime
- Azure CLI — For deployment and resource management
Microsoft Reference: Set up VS Code for Logic Apps
Project Initialisation
# Create project via VS Code
# 1. Open Command Palette (Ctrl+Shift+P)
# 2. Select "Azure Logic Apps: Create New Project"
# 3. Choose project folder
# 4. Select "Stateful Workflow" or "Stateless Workflow"
Project Structure
logic-app-project/
├── .vscode/
│ ├── extensions.json # Recommended extensions
│ ├── launch.json # Debug configuration
│ ├── settings.json # Workspace settings
│ └── tasks.json # Build tasks
├── Artifacts/
│ ├── Maps/ # XSLT maps for data transformation
│ └── Schemas/ # XML/JSON schemas for validation
├── lib/
│ └── custom/ # Custom connector assemblies (.NET)
├── workflow1/
│ └── workflow.json # Workflow definition
├── workflow2/
│ └── workflow.json # Another workflow definition
├── connections.json # Connector configurations
├── host.json # Runtime configuration
├── local.settings.json # Local environment variables
└── parameters.json # Workflow parameters
host.json
Configure runtime behaviour:
{
"version": "2.0",
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle.Workflows",
"version": "[1.*, 2.0.0)"
},
"extensions": {
"workflow": {
"settings": {
"Runtime.FlowRunRetryableActionJobCallback.MaximumRetries": "3",
"Runtime.Backend.FlowDefaultTimeout": "00:05:00"
}
}
},
"logging": {
"logLevel": {
"default": "Information",
"Host.Triggers.Workflow": "Debug"
},
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"maxTelemetryItemsPerSecond": 20
}
}
}
}
connections.json
Define connections for managed connectors:
{
"managedApiConnections": {
"office365": {
"api": {
"id": "/subscriptions/{sub}/providers/Microsoft.Web/locations/{location}/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"
},
"displayName": "Service Bus Connection"
}
}
}
local.settings.json
Local development environment variables:
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "node",
"WORKFLOWS_SUBSCRIPTION_ID": "{subscription-id}",
"WORKFLOWS_RESOURCE_GROUP_NAME": "{resource-group}",
"WORKFLOWS_LOCATION_NAME": "uksouth",
"WORKFLOWS_MANAGEMENT_BASE_URI": "https://management.azure.com/",
"ServiceBus_ConnectionString": "Endpoint=sb://...",
"SQL_ConnectionString": "Server=...",
"AppInsights_InstrumentationKey": "{key}"
}
}
Stateful vs Stateless Workflows
Stateful Workflows
- Run history persisted in storage account
- Input/output retention for debugging
- Durable execution — survives host restarts
- Higher latency due to storage operations
- Best for: Long-running processes, workflows needing audit trails
Stateless Workflows
- No run history persisted (in-memory only)
- Lower latency — no storage overhead
- Best for: High-throughput, request-response scenarios
- Cannot use: Chunking, managed connectors (built-in only), durable timers
Choosing Between Them
| Scenario | Stateful | Stateless |
|---|---|---|
| API orchestration (request-response) | Preferred | |
| Order processing with audit trail | Preferred | |
| Real-time data transformation | Preferred | |
| Long-running approval workflows | Preferred | |
| High-throughput event processing | Preferred | |
| B2B/EDI message processing | Preferred |
Parameters and App Settings
Workflow Parameters
Define reusable values across workflows in parameters.json:
{
"apiBaseUrl": {
"type": "String",
"value": "https://api.example.com/v1"
},
"maxRetries": {
"type": "Int",
"value": 3
},
"notificationEmail": {
"type": "String",
"value": "ops@example.com"
},
"enableDetailedLogging": {
"type": "Bool",
"value": true
}
}
Reference in workflows:
{
"HTTP_Call": {
"type": "Http",
"inputs": {
"method": "GET",
"uri": "@{parameters('apiBaseUrl')}/orders"
}
}
}
App Settings vs Parameters
| Feature | App Settings | Parameters |
|---|---|---|
| Scope | Application-wide | Per workflow |
| Change | Restart required | Workflow reload |
| Secrets | Key Vault references | Not recommended |
| CI/CD | Slot settings, ARM | parameters.json per environment |
Key Vault References in App Settings
@Microsoft.KeyVault(SecretUri=https://kv-logicapp-prod.vault.azure.net/secrets/ApiKey/)
@Microsoft.KeyVault(VaultName=kv-logicapp-prod;SecretName=ApiKey)
Microsoft Reference: Key Vault references
Local Debugging
Start Local Runtime
# Start Azurite storage emulator
azurite --silent --location .azurite --debug .azurite/debug.log
# Start Logic App runtime
func host start
VS Code Debug Configuration
.vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to Logic App",
"type": "coreclr",
"request": "attach",
"processId": "${command:azureLogicAppsStandard.pickProcess}"
}
]
}
Testing Workflows Locally
- Start the runtime with
func host start - For HTTP-triggered workflows, the local URL is displayed:
http://localhost:7071/api/workflow-name/triggers/manual/invoke?api-version=2020-05-01-preview - Send requests using curl, Postman, or the REST Client VS Code extension
- View run history in VS Code or the Azure portal (when connected)
Local Testing with Mock Data
Create test data files for trigger payloads:
// test-data/order-payload.json
{
"orderId": "ORD-001",
"customerId": "CUST-100",
"items": [
{ "productId": "PROD-A", "quantity": 2, "price": 29.99 }
],
"total": 59.98
}
curl -X POST http://localhost:7071/api/process-order/triggers/manual/invoke \
-H "Content-Type: application/json" \
-d @test-data/order-payload.json \
"?api-version=2020-05-01-preview"
Deployment
ZIP Deployment
# Build the project
dotnet publish -c Release -o ./publish
# Create ZIP package
cd publish && zip -r ../logic-app.zip . && cd ..
# Deploy to Azure
az logicapp deployment source config-zip \
--resource-group rg-integration-prod \
--name la-standard-prod \
--src logic-app.zip
Bicep Deployment
param location string = resourceGroup().location
param environment string
resource appServicePlan 'Microsoft.Web/serverfarms@2022-09-01' = {
name: 'asp-logicapp-${environment}'
location: location
sku: {
tier: 'WorkflowStandard'
name: environment == 'prod' ? 'WS2' : 'WS1'
}
properties: {
targetWorkerCount: environment == 'prod' ? 3 : 1
maximumElasticWorkerCount: environment == 'prod' ? 20 : 5
zoneRedundant: environment == 'prod'
}
}
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: 'stlogicapp${environment}'
location: location
sku: { name: 'Standard_ZRS' }
kind: 'StorageV2'
properties: {
minimumTlsVersion: 'TLS1_2'
supportsHttpsTrafficOnly: true
}
}
resource logicApp 'Microsoft.Web/sites@2022-09-01' = {
name: 'la-standard-${environment}'
location: location
kind: 'functionapp,workflowapp'
identity: {
type: 'SystemAssigned'
}
properties: {
serverFarmId: appServicePlan.id
httpsOnly: true
virtualNetworkSubnetId: subnetId
siteConfig: {
netFrameworkVersion: 'v6.0'
ftpsState: 'Disabled'
appSettings: [
{
name: 'AzureWebJobsStorage'
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=core.windows.net;AccountKey=${storageAccount.listKeys().keys[0].value}'
}
{
name: 'FUNCTIONS_EXTENSION_VERSION'
value: '~4'
}
{
name: 'FUNCTIONS_WORKER_RUNTIME'
value: 'node'
}
{
name: 'WEBSITE_NODE_DEFAULT_VERSION'
value: '~18'
}
{
name: 'APP_KIND'
value: 'workflowApp'
}
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
value: appInsights.properties.ConnectionString
}
]
}
}
}
Azure DevOps Pipeline for Standard Logic Apps
trigger:
branches:
include:
- main
paths:
include:
- src/logic-apps/*
variables:
- group: logic-app-settings
- name: workingDirectory
value: 'src/logic-apps'
stages:
- stage: Build
jobs:
- job: BuildLogicApp
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UseDotNet@2
inputs:
packageType: 'sdk'
version: '6.x'
- script: |
dotnet publish $(workingDirectory) -c Release -o $(Build.ArtifactStagingDirectory)/logic-app
displayName: 'Build Logic App'
- publish: $(Build.ArtifactStagingDirectory)/logic-app
artifact: logic-app
- stage: DeployDev
dependsOn: Build
jobs:
- deployment: DeployToDev
environment: 'logic-app-dev'
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: logic-app
- task: AzureFunctionApp@2
displayName: 'Deploy Logic App'
inputs:
azureSubscription: 'dev-service-connection'
appType: 'functionApp'
appName: 'la-standard-dev'
package: '$(Pipeline.Workspace)/logic-app/**/*.zip'
deploymentMethod: 'zipDeploy'
- stage: DeployProd
dependsOn: DeployDev
condition: succeeded()
jobs:
- deployment: DeployToProd
environment: 'logic-app-prod'
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: logic-app
- task: AzureFunctionApp@2
displayName: 'Deploy Logic App'
inputs:
azureSubscription: 'prod-service-connection'
appType: 'functionApp'
appName: 'la-standard-prod'
package: '$(Pipeline.Workspace)/logic-app/**/*.zip'
deploymentMethod: 'zipDeploy'
VNET Integration
Configure VNET Integration
resource logicApp 'Microsoft.Web/sites@2022-09-01' = {
// ...
properties: {
virtualNetworkSubnetId: integrationSubnet.id
vnetRouteAllEnabled: true // Route all traffic through VNET
siteConfig: {
vnetPrivatePortsCount: 2 // Private endpoints for outbound
}
}
}
// Private endpoint for inbound traffic
resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-05-01' = {
name: 'pe-logicapp-${environment}'
location: location
properties: {
subnet: {
id: privateEndpointSubnet.id
}
privateLinkServiceConnections: [
{
name: 'logicapp-connection'
properties: {
privateLinkServiceId: logicApp.id
groupIds: ['sites']
}
}
]
}
}
Network Architecture
Internet → (blocked by private endpoint)
APIM (VNET) → Private Endpoint → Logic App (VNET integrated)
Logic App → VNET Integration → Service Bus / SQL / Key Vault (via service endpoints or private endpoints)
Scaling and Performance
App Service Plan Sizing
| Plan | vCPU | Memory | Max Instances | Use Case |
|---|---|---|---|---|
| WS1 | 1 | 3.5 GB | 20 | Development, low-volume |
| WS2 | 2 | 7 GB | 20 | Production, medium-volume |
| WS3 | 4 | 14 GB | 20 | High-volume, complex workflows |
Autoscaling Rules
resource autoscale 'Microsoft.Insights/autoscalesettings@2022-10-01' = {
name: 'autoscale-logicapp'
location: location
properties: {
targetResourceUri: appServicePlan.id
enabled: true
profiles: [
{
name: 'Default'
capacity: {
minimum: '2'
maximum: '10'
default: '2'
}
rules: [
{
metricTrigger: {
metricName: 'CpuPercentage'
metricResourceUri: appServicePlan.id
timeGrain: 'PT1M'
statistic: 'Average'
timeWindow: 'PT5M'
operator: 'GreaterThan'
threshold: 70
}
scaleAction: {
direction: 'Increase'
type: 'ChangeCount'
value: '1'
cooldown: 'PT5M'
}
}
{
metricTrigger: {
metricName: 'CpuPercentage'
metricResourceUri: appServicePlan.id
timeGrain: 'PT1M'
statistic: 'Average'
timeWindow: 'PT10M'
operator: 'LessThan'
threshold: 30
}
scaleAction: {
direction: 'Decrease'
type: 'ChangeCount'
value: '1'
cooldown: 'PT10M'
}
}
]
}
]
}
}
Best Practices
- Use stateless workflows for high-throughput request-response scenarios
- Store secrets in Key Vault and reference via app settings
- Enable Application Insights for monitoring and diagnostics
- Use VNET integration for secure communication with backend services
- Implement CI/CD with Azure DevOps or GitHub Actions for consistent deployments
- Use parameters.json for environment-specific configuration
- Enable zone redundancy for production workloads
- Set up autoscaling based on CPU and memory metrics
- Use deployment slots for zero-downtime deployments
- Keep workflows focused — prefer multiple small workflows over one large workflow