Azure Lesson 47 of 137

Azure Logic Apps Standard: Stateful Workflows, VNet Integration, and B2B/EDI Integration Accounts

Logic Apps Standard is not “Consumption with a different SKU.” It is a different runtime — the single-tenant Azure Functions host running the Logic Apps engine as an extension — and that one fact drives almost every architectural decision you will make. You get a deployable package of code-like artifacts instead of a portal-bound resource, a dedicated compute plane you can drop into a virtual network, built-in connectors that run in-process, and a flat hosting price instead of per-action billing. The trade is that you now own things the Consumption tier hid from you: the storage account behind run history, the VNet plumbing, the deployment pipeline, and the integration-account lifecycle.

This guide builds a production-grade Standard logic app the way it survives an enterprise: stateful and stateless workflows side by side, locked behind private endpoints, exchanging EDI (AS2/X12/EDIFACT) with trading partners, and shipped through Bicep with deployment slots. The hardest failures in this space are quiet — a logic app that will not start because its storage lost a table private endpoint, a managed connector that silently can’t reach a private SQL server, a poison interchange that hot-loops your consumer. So beyond the prose and the az/Bicep snippets, every section anchors to scannable reference tables: the option matrices you set once, the limits you bump into, and a symptom → cause → confirm → fix playbook you keep open during a cutover.

By the end you will pick stateful versus stateless per workflow deliberately, wire all four storage sub-resources for private access, choose built-in over managed connectors for anything inside your VNet, load and reference B2B artifacts without the old Consumption link step, and ship the whole thing through a staging slot with slot-sticky settings. You will also know exactly which az command or portal blade proves each failure, so a stuck deployment is a ninety-second diagnosis instead of a two-hour one.

What problem this solves

Enterprise integration is where systems that were never meant to talk are forced to. An order placed in a partner’s ERP arrives as an X12 850 over AS2; it must be decoded, validated against an agreement, acknowledged with a 997, transformed, and written to a private order database — durably, with retries, and an audit trail a partner’s dispute can be settled against. Consumption Logic Apps handled the shape of this beautifully but hit two production walls: per-action billing turned high-fan-out EDI into a line item leadership noticed, and the multi-tenant connector model could not reach resources locked behind private endpoints, because connector traffic originates from Microsoft’s network, not yours.

What breaks without Standard’s model: teams either keep paying per-action and accept that a private-only data tier is impossible through managed connectors, or they hand-roll the whole pipeline in Functions and re-implement AS2/EDI from scratch. Both are expensive in different currencies. Standard collapses the gap — in-process B2B operations, VNet-aware built-in connectors, flat compute — but it hands you the operational surface the platform used to hide: storage you must keep reachable, networking you must configure in both directions, and a runtime that simply will not boot if its content share can’t mount.

Who hits this: integration teams moving EDI/B2B workloads off Consumption for cost or network-isolation reasons; platform teams adopting a hub-spoke topology where every PaaS dependency must be private; and anyone who tried to lock down a Standard logic app’s storage by copying an App Service runbook and watched the app refuse to start. The fix is rarely “open it back up” — it’s “find the sub-resource, setting, or DNS zone that’s missing and make the runtime’s dependency reachable.”

To frame the whole field before the deep dive, here is the spread of decisions this article makes concrete, the question each forces, and where the answer lives:

Decision area The question it forces Default that bites Where you set it
Hosting plan Dedicated compute or elastic? Isolated? WS1 may be undersized for B2B az functionapp plan SKU
Per-workflow kind Durable or in-memory? Stateful everywhere (storage tax) kind in workflow.json
Run-history storage Same region? Dedicated? Private? Shared account, public, no retention Storage account + host.json
VNet — outbound Route all egress through the VNet? WEBSITE_VNET_ROUTE_ALL=0 (RFC1918 only) App setting + integration
VNet — inbound Private trigger endpoint + lockdown? Public trigger, Allow default Private endpoint + access restriction
Connectors Built-in or managed? Managed can’t reach private targets connections.json
B2B artifacts Which integration-account SKU? Free/Basic can’t hold agreements Microsoft.Logic/integrationAccounts
Error handling Retry, try/catch, dead-letter? Default retry on non-idempotent ops workflow.json actions
CI/CD Slot swap with sticky settings? Slot loses VNet + steals prod creds Pipeline + slot settings

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should be comfortable with the App Service / Functions hosting model — an App Service (or Workflow Standard) plan is the set of VM workers you rent, and the logic app runs on it sharing its CPU and memory — because a Standard logic app is a function app with kind functionapp,workflowapp. You should know how to run az in Cloud Shell, read JSON output, and understand private endpoints, private DNS zones, and managed identity at a working level. Familiarity with EDI concepts (interchange, transaction set, control numbers) helps for the B2B section but is not assumed.

This sits in the Integration & Networking track. Upstream of it are the platform mechanics in the Azure App Service Deep Dive: Plans, Scaling, Slots, TLS (the plan, slots and VNet integration are shared) and the storage fundamentals in Azure Storage Accounts Deep Dive (run history lives there). It pairs tightly with Azure Service Bus sessions, dedup & dead-letter patterns for the messaging backbone, private endpoint vs service endpoint and private endpoints & private DNS at scale for the network isolation, and Azure Monitor & Application Insights for observability for the telemetry. If you are weighing this against code-first orchestration, Azure Durable Functions orchestration patterns is the sibling decision, and the EDI/B2B integration platform on Logic Apps article goes deeper on the trading-partner side.

A quick map of who owns what during an integration incident, so you call the right person fast:

Layer What lives here Who usually owns it Failure classes it can cause
Trading partner Their ERP, AS2 endpoint, certs Partner / EDI team Bad interchange, expired partner cert, MIC mismatch
Logic app workflow Triggers, actions, scopes Integration / dev team Action failure, bad retry, unhandled exception
Built-in connectors In-process SB/SQL/Blob Integration team Auth (MI), throttling, connection config
Run-history storage Tables/blobs/queues/files Platform team App won’t start, content share won’t mount
VNet / DNS Subnets, PEs, private zones Network team No resolution, managed connector can’t reach private
Integration account Partners, agreements, schemas EDI / integration team Decode fails, agreement not found, SKU gate
CI/CD + slots Pipeline, slot settings Platform / DevOps Leaked creds on swap, slot left VNet

Core concepts

Six mental models make every later decision obvious.

The resource is a function app wearing a Logic Apps hat. One Standard logic app hosts many workflows, each a workflow.json in its own folder. The resource — not the workflow — is the unit of deployment, scaling, networking, and identity. The az logicapp command group is a thin wrapper over az functionapp; when an operation is missing under logicapp, the functionapp equivalent works because the kind is literally functionapp,workflowapp. Internalise this and the whole platform stops being mysterious: VNet integration, slots, app settings, and Kudu all behave exactly as they do for Functions.

Billing is by allocated compute, not per action. You rent a plan (WS1/WS2/WS3 elastic, or App Service plan, or ASEv3 for full isolation) and pay for vCPU and memory whether it runs one action or ten million. A chatty, high-fan-out workflow is dramatically cheaper here; a workflow that fires ten times a day is cheaper on Consumption. This inversion is the entire economic case for migrating EDI.

Stateful versus stateless is a per-workflow durability contract. Stateful persists every run, every action input/output, and trigger history to external storage — you get durable run history, full inspection, resubmit, and mid-run survival across host restarts. Stateless runs entirely in memory — no persisted history, far lower latency, higher throughput, but no durability and run history off by default. The right pattern is usually both: a stateless front door for low-latency intake calling a stateful core for durable processing.

Built-in connectors run in your process; managed connectors run in Microsoft’s. Built-in (service-provider) connectors — Service Bus, SQL, Blob, Event Hubs, Cosmos DB, and the B2B operations — execute in-process on your plan and therefore honour VNet integration. Managed (API-connection) connectors egress from Microsoft’s multi-tenant connector service, so they cannot reach a private endpoint, and they bill per call. For anything inside your VNet, built-in is not a preference, it’s a requirement.

The storage account is on the hot path of every stateful run. The runtime keeps run history in Azure Storage tables and blobs, mounts its content share from Azure Files, and uses queues for internal dispatch. Lock that storage down wrong and the app won’t even start — it needs blob, file, table, and queue reachable, plus vnetContentShareEnabled to mount the content share over the VNet, plus the four privatelink DNS zones. This is the single most common Standard cutover failure.

B2B operations are in-process, but the artifacts still live in an integration account. In Standard, AS2 (v2), X12, and EDIFACT encode/decode run on your plan — but trading partners, agreements, schemas, maps, and certificates live in an integration account that, unlike Consumption, does not need to be linked to the logic app. Same subscription and region, referenced by name, no link step.

The vocabulary in one table

Before the deep sections, pin down every moving part. The glossary repeats these for lookup; this is the mental model side by side:

Term One-line definition Where it lives Why it matters
Logic app (Standard) Function app hosting many workflows Subscription / RG Unit of deploy, scale, network, identity
Workflow One workflow.json definition Folder in the project The runnable unit; has a kind
Stateful Persists runs to storage Storage tables/blobs Durable, inspectable, resubmittable
Stateless Runs in memory only The host process Fast, cheap, no durability
Plan (WS1/WS2/WS3) Elastic single-tenant compute The plan resource Sizing, scale-out, isolation
Built-in connector In-process service-provider op Your plan Honours VNet; included in compute
Managed connector Multi-tenant API connection Microsoft infra Can’t reach private; per-call billing
Integration account Store of B2B artifacts Same sub + region Partners, agreements, schemas, certs
vnetContentShareEnabled Mount content share over VNet Site property App won’t start on private storage without it
VNet integration Outbound through a subnet App + delegated subnet Reach private targets from workflows
Private endpoint Private inbound NIC A subnet Lock trigger + storage to the VNet
Managed identity Keyless Entra identity The logic app Data-plane RBAC; no secrets to rotate
AS2 / X12 / EDIFACT B2B transport / EDI dialects In-process operations Decode, validate, ack, encode
Functional ack (997/CONTRL) Receipt for an interchange Generated by decode Tells a partner accept/reject

Standard vs Consumption: the single-tenant runtime and pricing model

In Consumption, each logic app is one workflow, defined in ARM, billed per action execution, sharing a multi-tenant runtime you do not control. Standard inverts all of that. The clearest way to see the difference is feature by feature:

Dimension Consumption Standard
Runtime Multi-tenant, Microsoft-managed Single-tenant Functions host you provision
Workflows per resource One Many (one workflow.json each)
Billing Per action execution + connector calls Allocated compute (vCPU/memory)
Compute control None WS plan / App Service plan / ASEv3
Built-in connectors Limited Rich, in-process, VNet-aware
VNet integration No (managed connectors egress from MS) Yes (regional integration)
Private inbound No Private endpoint on the app
Deployment unit ARM template (portal-bound) Project zip (code-like)
Deployment slots No Yes
Integration account Must be linked Referenced, not linked
Stateless workflows No Yes
Local development Limited Full (func, VS Code designer)
Best for Low-volume, spiky, SaaS-glue High-volume, private, B2B, code-like CI/CD

The decision rule, as a table you can hand to an architect:

If your workload is… Pick Because
A few runs/day, public SaaS only Consumption Per-action billing is near-free at low volume
High-fan-out EDI / millions of cheap actions Standard Flat compute makes the marginal action free
Must reach private-endpoint-only data Standard Built-in connectors honour VNet integration
Needs durable + low-latency paths together Standard Stateful + stateless side by side
Code-like CI/CD, slots, local dev Standard Project zip, slots, func tooling
Bursty, unpredictable, no network needs Consumption No idle compute to pay for

A bare-bones provisioning path — App Service (or WS) plan plus the logic app:

RG=rg-integration-prod
LOC=westeurope
PLAN=asp-logicapps-prod
APP=la-orders-prod
SA=stlaordersprod$RANDOM   # storage backs run history + content

az group create -n $RG -l $LOC

az storage account create -n $SA -g $RG -l $LOC \
  --sku Standard_LRS --kind StorageV2 --min-tls-version TLS1_2 \
  --allow-blob-public-access false

# Workflow Standard plan (elastic) for single-tenant Logic Apps
az functionapp plan create -n $PLAN -g $RG -l $LOC \
  --sku WS1 --is-linux false

az logicapp create -n $APP -g $RG -l $LOC \
  --plan $PLAN --storage-account $SA \
  --functions-version 4

The az logicapp command group is a thin wrapper over az functionapp. Many operations you cannot find under az logicapp exist under az functionapp and work fine, because a Standard logic app is a function app with a Logic Apps extension bundle and a kind of functionapp,workflowapp.

Choose the hosting plan deliberately — it sets your compute ceiling, scale behaviour, and isolation:

Plan Compute model vCPU / RAM (approx) Scale-out Network isolation When to pick
WS1 Elastic (Workflow Standard) 1 vCPU / 3.5 GB Auto, to 20 Regional VNet integration Most production workloads, moderate volume
WS2 Elastic 2 vCPU / 7 GB Auto, to 20 Regional VNet integration Heavier EDI, larger payloads, more parallelism
WS3 Elastic 4 vCPU / 14 GB Auto, to 20 Regional VNet integration High throughput, big-message transforms
App Service plan (P1v3+) Dedicated 2+ vCPU / 8+ GB Manual / autoscale Regional VNet integration Predictable load, shared with other apps
ASEv3 (Isolated v2) Dedicated, isolated Large High Native, private by default Hard compliance / single-tenant network needs

The local project that this resource deploys is just a folder. func init with the workflow worker, or the VS Code “Azure Logic Apps (Standard)” extension, scaffolds:

my-logic-app/
  host.json                 # runtime settings for the whole resource
  local.settings.json       # local-only app settings (do NOT deploy secrets)
  connections.json          # managed + service-provider connection refs
  parameters.json           # workflow parameters (environment-substitutable)
  Orders-Intake/
    workflow.json           # one workflow definition
  EDI-Inbound/
    workflow.json
  workflow-designtime/       # design-time host used by the designer only
    host.json
    local.settings.json

Each file in that layout has a single job — and a single way it bites you if you get it wrong:

File Purpose Deployed? Gotcha
host.json Runtime + extension-bundle settings for the resource Yes Pins the bundle that carries the engine + built-ins
local.settings.json Local-only app settings No Never commit secrets; mirror keys as real app settings
connections.json Connection refs (built-in + managed) Yes Built-in: serviceProviderConnections; managed: managedApiConnections
parameters.json Workflow parameters Yes Keep env-specific values here, substitute at deploy
<workflow>/workflow.json One workflow definition Yes Carries kind (Stateful/Stateless)
workflow-designtime/ Designer-only host No Local design experience; not a runtime artifact

The host.json pins the extension bundle that carries the Logic Apps runtime and all built-in connectors:

{
  "version": "2.0",
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle.Workflows",
    "version": "[1.*, 2.0.0)"
  }
}

Stateful vs stateless workflows and run-history storage design

Every workflow declares a kind: Stateful or Stateless. This is the single most consequential per-workflow choice you make, and it is irreversible only in the sense that switching changes behaviour you may depend on. Lay the two side by side:

Aspect Stateful Stateless
Run history Persisted to storage (tables/blobs) None by default (debug-only opt-in)
Mid-run durability Survives host restart Lost on restart
Latency Higher (checkpoints to storage) Low (in-memory)
Throughput Lower per instance Higher per instance
Resubmit / inspect runs Yes No
Until loop durability Yes No (no cross-restart durability)
Chunking / large messages Yes No
Best for Orchestration, async, human-in-loop, EDI core Short synchronous request/response, validate-and-forward
Storage cost Accumulates run history Negligible

A minimal stateful definition header:

{
  "definition": {
    "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
    "contentVersion": "1.0.0.0",
    "triggers": { },
    "actions": { },
    "outputs": { }
  },
  "kind": "Stateful"
}

Picking which to use, by trigger and intent:

If the workflow… Use Why
Decodes/validates/persists an EDI interchange Stateful Durability + resubmit on partner dispute
Validates a webhook and forwards in <1s Stateless Latency and throughput; no durability needed
Waits on an approval / external callback Stateful Must survive across the wait and host restarts
Fans out to many parallel branches with Until Stateful Loop durability across restarts
Is a thin front door calling a stateful core Stateless Cheap, fast intake; durability lives downstream
Handles large (MB) payloads needing chunking Stateful Stateless cannot chunk

Run-history storage design is your responsibility now. The storage account is on the hot path of every stateful run; treat it as a first-class dependency. Stateless can enable run history for debugging only, at a throughput cost, via Workflows.<name>.OperationOptions = WithStatelessRunHistory.

{
  "version": "2.0",
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle.Workflows",
    "version": "[1.*, 2.0.0)"
  },
  "extensions": {
    "workflow": {
      "settings": {
        "Runtime.Backend.Run.MaxRunHistoryRetentionInDays": "90"
      }
    }
  }
}

What each storage sub-resource is used for, so you know why all four must be reachable when you go private:

Storage service What the runtime uses it for Lock-down impact if missing
Blob Run-history payloads, large action I/O Run history broken; large messages fail
File The app’s content share (the deployed code) App won’t start — content share can’t mount
Table Run/trigger state, checkpoints, leases App won’t start; no durable state
Queue Internal job dispatch between host and engine Triggers/actions don’t dispatch

The host settings worth knowing for run-history and dispatcher behaviour:

Host setting What it controls Typical value When to change
Runtime.Backend.Run.MaxRunHistoryRetentionInDays Days run history is kept 90 Compliance retention or cost control
Runtime.Trigger.MaximumWaitingRuns Cap on queued runs per workflow Engine default Bursty triggers overrunning capacity
Runtime.Backend.VariableOperation.MaximumStatelessVariableSize Max variable size (stateless) Engine default Larger in-memory variables
Workflows.<name>.OperationOptions Per-workflow behaviour flags unset WithStatelessRunHistory for stateless debug
Runtime.FlowMaintenanceJob.* Cleanup/prune cadence Engine defaults Tuning prune aggressiveness

A useful default: make the fast synchronous edges stateless and the orchestration core stateful, and have the stateless front door call the stateful workflow. You get low-latency intake and durable processing without paying the storage tax on every health check.

VNet integration, private endpoints, and access restrictions

Enterprise Standard deployments are almost always private. There are two independent traffic directions, and you configure them separately — confusing them is a top-three mistake.

Direction What it covers Mechanism Key control
Outbound Your workflow reaching private SQL/SB/storage Regional VNet integration (delegated subnet) WEBSITE_VNET_ROUTE_ALL=1
Inbound Clients reaching your workflow triggers Private endpoint on the logic app Access restriction --default-action Deny
Runtime storage The app reaching its own run history Storage private endpoints + content share vnetContentShareEnabled=true + DNS

Outbound (your workflow reaching private resources) uses regional VNet integration — the same mechanism as App Service. Outbound calls to a private SQL, Service Bus, or storage endpoint route through a delegated subnet:

VNET=vnet-integration
SUBNET=snet-logicapps     # delegated to Microsoft.Web/serverFarms, /26 or larger

az functionapp vnet-integration add -g $RG -n $APP \
  --vnet $VNET --subnet $SUBNET

# Force all outbound traffic through the VNet (not just RFC1918)
az functionapp config appsettings set -g $RG -n $APP \
  --settings WEBSITE_VNET_ROUTE_ALL=1

The subnet has hard requirements — get any of these wrong and integration silently fails or starves you of IPs:

Subnet requirement Value Why
Delegation Microsoft.Web/serverFarms Required for regional VNet integration
Size /26 or larger The plan consumes IPs per instance as it scales
Dedicated One integration per subnet Cannot share with other delegated services
Same region As the plan and VNet Regional integration is region-bound
WEBSITE_VNET_ROUTE_ALL 1 for full egress 0 routes only RFC1918; public targets bypass the VNet

Inbound (clients reaching your workflow triggers) uses a private endpoint on the logic app, plus public-access lockdown:

az network private-endpoint create -g $RG -n pe-$APP \
  --vnet-name $VNET --subnet snet-private-endpoints \
  --private-connection-resource-id $(az logicapp show -g $RG -n $APP --query id -o tsv) \
  --group-id sites --connection-name conn-$APP

az functionapp config access-restriction set -g $RG -n $APP \
  --default-action Deny

The storage account is the part teams forget. A Standard logic app cannot start if it cannot reach its own run-history storage. When you lock the storage account behind private endpoints, you must also tell the runtime to mount its content share over the VNet. The modern control is the site property vnetContentShareEnabled (which superseded the WEBSITE_CONTENTOVERVNET=1 app setting). You need private endpoints for all four storage sub-resources — blob, file, table, and queue:

for SUB in blob file table queue; do
  az network private-endpoint create -g $RG -n pe-$SA-$SUB \
    --vnet-name $VNET --subnet snet-private-endpoints \
    --private-connection-resource-id $(az storage account show -g $RG -n $SA --query id -o tsv) \
    --group-id $SUB --connection-name conn-$SA-$SUB
done

# Mount content share over the VNet (required for private storage)
az resource update -g $RG -n $APP \
  --resource-type "Microsoft.Web/sites" \
  --set properties.vnetContentShareEnabled=true

Each storage sub-resource needs its matching private DNS zone linked to the VNet, or name resolution fails before networking does:

Sub-resource Private endpoint group-id Private DNS zone
Blob blob privatelink.blob.core.windows.net
File file privatelink.file.core.windows.net
Table table privatelink.table.core.windows.net
Queue queue privatelink.queue.core.windows.net
Logic app (inbound) sites privatelink.azurewebsites.net

When you secure storage this way, every app sharing that account must use the same vnetContentShareEnabled value, and you still need the corresponding private DNS zones linked to the VNet, or name resolution fails before networking does. For the DNS design at scale, see private endpoints & private DNS at scale.

The access-restriction model has more than the binary default action — here is the full surface:

Control What it does Values Note
--default-action Allow/deny when no rule matches Allow / Deny Set Deny for private-only
IP rules Allow specific CIDRs CIDR list + priority For known caller ranges
Service-tag rules Allow an Azure service tag tag + priority E.g. a specific PaaS source
Private endpoint Inbound over the VNet PE on sites Bypasses public path entirely
SCM site restrictions Lock Kudu/deploy separately own rule set Often Deny public + pipeline IP allow

Built-in vs managed connectors and authentication patterns

Standard exposes two connector families, and choosing correctly is both a cost and a networking decision.

Aspect Built-in (service provider) Managed (API connection)
Where it runs In-process on your plan Multi-tenant connector infra
Networking Honors VNet integration Egresses from Microsoft, not your VNet
Config lives in connections.json (serviceProviderConnections) connections.json (managedApiConnections) + ARM resource
Cost Included in plan compute Per-call execution billing
Auth App settings / managed identity Connection-level auth, often a consent grant
Reaches private endpoints Yes No
Throughput High (local) Bounded by connector throttling

Prefer built-in connectors for anything in your VNet. A managed connector cannot reach a private endpoint, because its traffic originates from Microsoft’s connector service, not your integrated subnet. A built-in Service Bus or SQL connector runs on your plan and respects WEBSITE_VNET_ROUTE_ALL. The common targets and which family to use:

Target Built-in available? Recommendation Why
Service Bus Yes Built-in VNet-aware, MI auth, high throughput
SQL Server / Azure SQL Yes Built-in Reaches private endpoint; MI auth
Blob / ADLS Yes Built-in VNet-aware; large I/O
Event Hubs Yes Built-in VNet-aware streaming
Cosmos DB Yes Built-in VNet-aware; MI auth
AS2 / X12 / EDIFACT Yes Built-in In-process B2B operations
HTTP (generic) Yes Built-in Honours VNet for private URLs
Office 365 / Outlook No Managed Public SaaS; no private path needed
Salesforce / ServiceNow No Managed Public SaaS
SAP Yes (built-in) Built-in VNet-aware on-prem/private SAP

Built-in connections are pure config in connections.json, with secrets pulled from app settings:

{
  "serviceProviderConnections": {
    "serviceBus": {
      "parameterValues": {
        "fullyQualifiedNamespace": "@appsetting('SB_NAMESPACE')"
      },
      "serviceProvider": { "id": "/serviceProviders/serviceBus" },
      "displayName": "Orders namespace"
    }
  }
}

For authentication, use managed identity end to end. Assign a system- or user-assigned identity to the logic app and grant it data-plane RBAC on the targets — no connection strings, no shared keys to rotate (the broader pattern is in Key Vault secret rotation with managed identity):

az logicapp identity assign -g $RG -n $APP

PRINCIPAL=$(az logicapp identity show -g $RG -n $APP --query principalId -o tsv)
SB_ID=$(az servicebus namespace show -g $RG -n ns-orders --query id -o tsv)

az role assignment create --assignee $PRINCIPAL \
  --role "Azure Service Bus Data Receiver" --scope $SB_ID

The least-privilege data-plane roles you grant the identity, per target:

Target Role for receive/read Role for send/write Scope
Service Bus Azure Service Bus Data Receiver Azure Service Bus Data Sender Namespace or entity
Storage (Blob) Storage Blob Data Reader Storage Blob Data Contributor Account or container
Storage (Queue) Storage Queue Data Reader Storage Queue Data Message Sender Account or queue
Azure SQL (db user mapped to MI) db_datareader db_datawriter Database
Event Hubs Azure Event Hubs Data Receiver Azure Event Hubs Data Sender Namespace or hub
Cosmos DB Cosmos DB Built-in Data Reader Cosmos DB Built-in Data Contributor Account (data plane)
Key Vault Key Vault Secrets User Vault

System-assigned versus user-assigned identity, because the choice affects CI/CD:

Aspect System-assigned User-assigned
Lifecycle Tied to the logic app Independent resource
Reuse across apps No Yes (share one identity)
Pre-grant RBAC before deploy Hard (principal exists after create) Easy (create identity first)
Slot behaviour Each slot has its own Shared if assigned to both
Best for Single app, simple Fleets, pre-provisioned RBAC, slots

The built-in connector then authenticates with the identity by referencing it in the connection’s authentication block, so the only thing in local.settings.json is the fully qualified namespace, never a key.

B2B messaging: AS2, X12, EDIFACT, and integration accounts

This is where Standard genuinely diverges from Consumption, and where a lot of stale documentation will mislead you. In Standard, AS2 (v2), X12, and EDIFACT are built-in operations that run in-process — encode, decode, and the AS2 send/receive pipeline all execute on your plan. But the artifacts those operations need — trading partners, agreements, schemas, maps, and certificates — still live in an integration account. The critical change: in Standard, the integration account does not need to be linked to the logic app resource. You reference it, but the old “link the integration account to the logic app” step from Consumption is gone. It must still be in the same subscription and region as the logic app.

The B2B operations available in-process and what each does:

Operation Direction What it does Key output
AS2 decode Inbound Decrypt, verify signature, check MIC Payload + MDN disposition
AS2 encode Outbound Sign, encrypt, package per agreement AS2 message + MDN request
X12 decode Inbound Parse interchange, validate, ack Payload + 997/TA1
X12 encode Outbound Build interchange from data + agreement X12 message
EDIFACT decode Inbound Parse, validate, ack Payload + CONTRL
EDIFACT encode Outbound Build interchange EDIFACT message
Transform (Liquid/XSLT/map) Either Map between schemas Transformed payload

The artifacts you load into the integration account, and which operation needs each:

Artifact What it is Needed by Format
Partner Identity + qualifiers (e.g. ZZ, 01) All B2B Business identity
Agreement Per-partner protocol settings Decode/encode AS2/X12/EDIFACT config
Schema EDI structure (e.g. X12 850) Decode validation XSD
Map Source→target transform Transform XSLT / Liquid
Certificate Signing/encryption keys AS2 Public/private cert

Provision the integration account and load artifacts:

az resource create -g $RG -n ia-edi-prod \
  --resource-type Microsoft.Logic/integrationAccounts \
  --location $LOC \
  --properties '{"sku":{"name":"Standard"}}'

# Upload an X12 schema (artifacts: schemas, partners, agreements, certs)
az logic integration-account schema create \
  -g $RG --integration-account ia-edi-prod \
  --schema-name X12_850_Schema \
  --content-type application/xml \
  --schema-type Xml \
  --input-path ./artifacts/X12_00401_850.xsd

Pick the SKU deliberately — this is a hard gate, not a soft preference:

SKU Agreements (X12/EDIFACT trading) Artifacts B2B tracking Use for
Free No Tiny limit No Throwaway dev only
Basic No Holds artifacts No Storing schemas/maps, no real trading
Standard Yes Large count No Full X12/EDIFACT trading (most prod)
Premium-level (for tracking) Yes Large Yes (via Azure Data Explorer) Transaction-level B2B tracking

Basic holds artifacts but does not support agreements — so no real X12/EDIFACT trading. B2B tracking for Standard workflows requires a Premium-level integration account — that is the tier gate for getting transaction-level tracking surfaced via Azure Data Explorer.

A decode step references the agreement and validates structure, control numbers, and (for AS2) MIC and signatures:

{
  "Decode_X12_message": {
    "type": "ServiceProvider",
    "inputs": {
      "parameters": {
        "message": "@triggerBody()",
        "agreementName": "Contoso-to-Fabrikam-850"
      },
      "serviceProviderConfiguration": {
        "serviceProviderId": "/serviceProviders/x12",
        "operationId": "decodeX12Message",
        "connectionName": "x12"
      }
    }
  }
}

The decode action returns the parsed payload, a control-number ack disposition, and a generated 997 (X12) or CONTRL (EDIFACT) acknowledgment you route back to the partner. The acknowledgment types and what they mean:

Ack type Standard Scope Meaning
TA1 X12 Interchange Interchange-level receipt (envelope OK/bad)
997 X12 Functional group Accept/reject of transaction sets
999 X12 (HIPAA) Functional group Implementation ack with error detail
CONTRL EDIFACT Interchange/message Syntax acknowledgment
MDN AS2 Transport Receipt (signed/unsigned) for the AS2 message

Wire the functional ack into your error path so a partner gets a negative ack on a bad interchange rather than silence.

Error handling, retry policies, and dead-letter strategies

Logic Apps gives you three layers of failure control. Use all three; relying on any one alone leaves a gap.

Layer Scope Handles Limit
Action retry policy One action Transient dependency blips Re-runs the same action; bad for non-idempotent ops
Try/Catch scope (runAfter) A block of actions Logical failure handling, compensation Within one run; doesn’t persist the message
Broker dead-letter The message itself Poison messages across retries Needs a broker (SB/Event Hubs) + a drain workflow

1. Action-level retry policy. Every action supports a retry policy. Default is exponential, four retries. Tune it explicitly on anything that touches a flaky dependency, and set it to none on non-idempotent operations you do not want re-attempted. The retry-policy options:

Field Meaning Values Default
type Retry strategy none / fixed / exponential exponential
count Number of retries 0–90 4
interval Base interval (ISO 8601) e.g. PT10S varies
minimumInterval Floor for exponential ISO 8601
maximumInterval Ceiling for exponential ISO 8601
{
  "Call_partner_endpoint": {
    "type": "Http",
    "inputs": {
      "method": "POST",
      "uri": "@parameters('partnerUrl')",
      "retryPolicy": {
        "type": "exponential",
        "count": 4,
        "interval": "PT10S",
        "maximumInterval": "PT1H",
        "minimumInterval": "PT10S"
      }
    }
  }
}

When to use which retry type:

Scenario Retry type Rationale
Idempotent GET to a flaky API exponential Back off through transient outages
Rate-limited dependency (429) exponential w/ longer interval Respect throttling windows
Fixed-cadence poll fixed Predictable retry timing
Non-idempotent POST (charge, create) none Avoid duplicate side effects
Operation guarded by idempotency key exponential Safe to retry; dedup handles dups

2. Scopes with runAfter for try/catch. Wrap a body of actions in a Scope, then add a handler scope that runs only when the first one fails, via runAfter statuses. This is the canonical Logic Apps try/catch. The runAfter statuses you compose with:

Status Means Use in catch
Succeeded Action completed OK Normal forward path
Failed Action threw/errored Catch handler
TimedOut Action exceeded its timeout Catch handler
Skipped Upstream didn’t run Rarely; cleanup branches
{
  "Try": { "type": "Scope", "actions": { "Process_order": { } } },
  "Catch": {
    "type": "Scope",
    "runAfter": { "Try": ["Failed", "TimedOut"] },
    "actions": {
      "Send_to_deadletter": {
        "type": "ServiceProvider",
        "inputs": {
          "parameters": { "entityName": "orders-deadletter", "message": "@result('Try')" },
          "serviceProviderConfiguration": {
            "serviceProviderId": "/serviceProviders/serviceBus",
            "operationId": "sendMessage",
            "connectionName": "serviceBus"
          }
        }
      }
    }
  }
}

@result('Try') returns the full action results inside the failed scope — inputs, outputs, status, error — which is exactly what you want to capture for a dead-letter record.

3. Dead-letter the message, not just the run. For event-driven intake (Service Bus, Event Hubs), let the broker’s native dead-letter queue do its job: do not auto-complete a message until processing succeeds, so a thrown exception abandons it back to the queue and, after max delivery count, lands it in the DLQ. Pair that with a separate “DLQ drain” workflow that reads the dead-letter subqueue, enriches with the failure reason, and pushes to a triage topic. The result: a poison EDI interchange parks itself instead of hot-looping your consumer. The Service Bus dead-letter mechanics (deep-dived in Service Bus sessions, dedup & dead-letter):

Mechanism What triggers it Where it lands Recovery
Max delivery count Repeated abandon/lock-loss $DeadLetterQueue subqueue Drain workflow reads + triages
Explicit dead-letter Code/workflow rejects message DLQ with reason + description Inspect reason, fix, resubmit
TTL expiry Message older than TTL DLQ (if dead-letter-on-expiry on) Usually drop or alert
Auto-complete off Exception before complete Back to queue, then DLQ at max The safe default for poison handling

CI/CD with deployment slots, parameters, and ARM/Bicep packaging

Standard logic apps deploy as a zip of the project folder — the same artifact you run locally. That makes CI/CD genuinely code-like. Three rules govern it:

Rule What it means Failure if ignored
Parameterize for environments Env values in parameters.json / app settings, not workflow.json Prod creds baked into a workflow
Provision infra with Bicep, identity-first IaC for the app, plan, storage, RBAC Drift; manual identity grants
Deploy code via slot swap Zip to staging, smoke-test, swap In-place deploy = downtime + risk

Provision infrastructure with Bicep, identity-first:

param location string = resourceGroup().location
param appName string
param planId string
param storageName string

resource logicApp 'Microsoft.Web/sites@2023-12-01' = {
  name: appName
  location: location
  kind: 'functionapp,workflowapp'
  identity: { type: 'SystemAssigned' }
  properties: {
    serverFarmId: planId
    vnetContentShareEnabled: true
    siteConfig: {
      vnetRouteAllEnabled: true
      appSettings: [
        { name: 'FUNCTIONS_EXTENSION_VERSION', value: '~4' }
        { name: 'FUNCTIONS_WORKER_RUNTIME', value: 'node' }
        {
          name: 'AzureWebJobsStorage'
          value: 'DefaultEndpointsProtocol=https;AccountName=${storageName};AccountKey=${listKeys(resourceId('Microsoft.Storage/storageAccounts', storageName), '2023-01-01').keys[0].value};EndpointSuffix=core.windows.net'
        }
        { name: 'APP_KIND', value: 'workflowApp' }
      ]
    }
  }
}

The app settings a Standard logic app actually depends on — what each does and the value that bites:

App setting Purpose Typical value Gotcha
FUNCTIONS_EXTENSION_VERSION Functions host major version ~4 Older versions miss the workflow bundle
FUNCTIONS_WORKER_RUNTIME Worker stack node / dotnet Must match the project
APP_KIND Marks it a workflow app workflowApp Wrong/missing → designer/runtime issues
AzureWebJobsStorage Run-history storage connection conn string / MI ref Lose it → app won’t start
WEBSITE_VNET_ROUTE_ALL Full egress through VNet 1 0 leaks public traffic out of VNet
WEBSITE_CONTENTSHARE Content share name auto Don’t hand-edit on a running app
APPLICATIONINSIGHTS_CONNECTION_STRING Telemetry target conn string Missing → no run telemetry
WORKFLOWS_SUBSCRIPTION_ID / _RESOURCE_GROUP_NAME Context for some ops ids Needed by certain built-ins

Deploy code with slots for zero-downtime cutover. Push to a staging slot, validate, then swap:

# one-time: create the staging slot
az functionapp deployment slot create -g $RG -n $APP --slot staging

# in the pipeline: zip-deploy the built project to staging
az functionapp deployment source config-zip \
  -g $RG -n $APP --slot staging --src ./output/app.zip

# smoke test the staging hostname here, then swap into production
az functionapp deployment slot swap -g $RG -n $APP --slot staging --target-slot production

Two slot caveats specific to Standard — both are silent until they hurt:

Caveat What goes wrong Fix
Settings move on swap Staging creds/endpoints follow into production Mark connection/endpoint settings slot-sticky (deployment-slot setting)
Slot has no VNet The slot’s outbound calls leave the VNet Add VNet integration to the slot’s subnet too
Slot identity differs System-assigned MI per slot lacks RBAC Use a shared user-assigned identity, or grant the slot’s MI
Warm-up skipped First post-swap runs hit cold workers Configure swap warm-up ping path

Mark connection-string and endpoint app settings as slot-sticky so a swap does not move staging credentials into production, and remember that a slot needs its own VNet integration — it is not inherited from production, so integrate the slot’s subnet or its outbound calls leave the VNet. The deployment methods compared:

Method Mechanism Atomic? Best for
config-zip to slot + swap Zip deploy, then swap Yes (swap) Production CI/CD
Run-from-package Immutable mounted package Yes Tamper-proof, fast rollback
VS Code publish IDE zip deploy No Dev/test only
func azure functionapp publish CLI zip deploy No Scripted dev deploys

Monitoring with Application Insights and run-time telemetry

Enable Application Insights at creation; the runtime emits structured telemetry for triggers, actions, and the host. Wire it through app settings (the full observability picture is in Azure Monitor & Application Insights):

AI_CONN=$(az monitor app-insights component show -g $RG -a ai-logicapps --query connectionString -o tsv)

az functionapp config appsettings set -g $RG -n $APP \
  --settings APPLICATIONINSIGHTS_CONNECTION_STRING="$AI_CONN"

How workflow telemetry maps to App Insights tables:

What you want to see Table Key columns
Workflow runs (trigger fired) requests name, success, duration, resultCode
Individual action executions dependencies / traces customDimensions.actionName, status
Failures / exceptions exceptions / traces message, operation_Name
Run/workflow correlation all customDimensions.resource_workflowName, run id

Workflow runs surface as requests, and individual action executions as dependencies and traces, each tagged with the workflow and run identifiers. This KQL pulls failed actions in the last day with their error, grouped by workflow:

traces
| where timestamp > ago(1d)
| extend wf = tostring(customDimensions["resource_workflowName"])
| extend action = tostring(customDimensions["actionName"])
| extend status = tostring(customDimensions["status"])
| where status == "Failed"
| summarize failures = count() by wf, action, message
| order by failures desc

For latency, chart the run duration distribution to catch a workflow whose p95 is creeping toward the host timeout:

requests
| where timestamp > ago(7d)
| where name has "workflow"
| summarize p50 = percentile(duration, 50),
            p95 = percentile(duration, 95),
            p99 = percentile(duration, 99)
            by bin(timestamp, 1h), name
| render timechart

The signals worth an alert, and what each catches before a partner notices:

Signal Metric / source Threshold idea Catches
Action/trigger failure rate Workflow Action/Trigger Failures >0 sustained Broken connector, bad agreement
Host 5xx Http5xx (plan) spike Host/plan degradation
Memory MemoryWorkingSet near SKU ceiling OOM risk on big transforms
Run latency p95 requests duration creeping up Throttled dependency, large payloads
DLQ depth Service Bus metric >0 / climbing Poison messages accumulating
Trigger backlog Runtime.Trigger waiting runs climbing Intake outrunning capacity

Architecture at a glance

The diagram traces a B2B interchange end to end, left to right, through the real Standard architecture and marks each numbered failure point where a cutover actually breaks. A trading partner sends an X12 850 over AS2 to the logic app’s private trigger endpoint (a private endpoint on sites, with public access set to Deny). Inside the logic app on a WS-series plan, two workflows cooperate: a stateless intake workflow validates and hands off to a stateful EDI core that runs the in-process AS2/X12 decode, references the integration account for the partner agreement and 850 schema, generates the 997 functional ack, and writes order state. Because the workflow uses built-in connectors over VNet integration (WEBSITE_VNET_ROUTE_ALL=1 through a delegated /26 subnet), it reaches the private SQL and Service Bus endpoints that a managed connector never could. Underneath everything sits the run-history storage the runtime cannot live without — all four sub-resources (blob, file, table, queue) reached over private endpoints with vnetContentShareEnabled and the matching privatelink DNS zones.

Read the badges as the failure map for a go-live. Badge 1 is the storage trap: miss the table/queue endpoint or vnetContentShareEnabled and the app won’t start at all. Badge 2 is the managed-connector mistake: point the core at a private SQL through a managed connection and it silently can’t reach it. Badge 3 is the B2B SKU gate: a Basic integration account holds schemas but rejects the agreement, so decode fails. Badge 4 is the poison-message hazard: auto-complete left on means a bad interchange hot-loops instead of dead-lettering. Badge 5 is the slot footgun: swap without slot-sticky settings or per-slot VNet integration and you either leak staging creds into production or push the slot’s egress out of the VNet. The legend narrates each as symptom · confirm · fix.

Azure Logic Apps Standard B2B architecture: a trading partner sends an X12 850 over AS2 to a private trigger endpoint on a Workflow Standard logic app; a stateless intake workflow hands off to a stateful EDI core that runs in-process AS2/X12 decode against an integration account holding the partner agreement and 850 schema, generates a 997 ack, and via built-in connectors over VNet integration writes to a private Azure SQL and a Service Bus topic, all backed by run-history storage with private endpoints for blob, file, table and queue plus vnetContentShareEnabled; numbered badges mark the five cutover failure points — storage not fully private so the app won't start, a managed connector that can't reach private SQL, a Basic integration account that rejects the agreement, a poison interchange hot-looping because auto-complete is on, and a slot swap that leaks credentials or drops VNet integration

Real-world scenario

Meridian Freight, a mid-size 3PL, ran EDI intake for retail trading partners on Logic Apps Consumption: AS2 receive, X12 850/856 decode, transform, and a write to an order database. Two engineers owned it; the monthly Logic Apps bill ran about ₹62,000 and nobody loved it. As a large retailer onboarded, two things broke at once. First, per-action billing on high-fan-out 850/856 processing — each interchange fanning into dozens of line-item actions — turned the bill into a number leadership started asking about, projected to roughly triple. Second, a new security baseline mandated that the SQL database holding order state become private-endpoint-only — which the Consumption tier’s managed SQL connector could not reach, because its traffic originates outside the customer’s network.

They migrated to Logic Apps Standard on a WS2 plan inside the platform’s hub-spoke VNet. The structural win was immediate on paper: the built-in SQL and Service Bus connectors run in-process and honour VNet integration, so order writes would flow over the private endpoint with no public exposure, and flat compute billing made the high-action EDI parsing essentially free at the margin. They kept their existing integration account (now unlinked, per the Standard model) for partner agreements and X12 schemas and moved AS2 decode in-process. They split the design: a stateless intake workflow for the AS2 receive and validation, calling a stateful core for decode, transform, ack generation, and the durable order write.

Then cutover went sideways in exactly the way this article warns about. The logic app would not start in the locked-down subscription — it sat in a restart loop with no useful application error. The storage account had been put behind private endpoints to meet the same baseline, but only the blob and file sub-resources had endpoints — the team had followed an App Service runbook. The Logic Apps runtime also needs table and queue storage for run history, and without vnetContentShareEnabled the content share would not mount. The fix was two pieces of infrastructure: private endpoints for all four sub-resources, and the site property flipped on.

// All four storage sub-resources need private endpoints for a Standard logic app
var storageGroups = [ 'blob', 'file', 'table', 'queue' ]

resource pe 'Microsoft.Network/privateEndpoints@2023-11-01' = [for g in storageGroups: {
  name: 'pe-${storageName}-${g}'
  location: location
  properties: {
    subnet: { id: privateEndpointSubnetId }
    privateLinkServiceConnections: [{
      name: 'conn-${g}'
      properties: {
        privateLinkServiceId: storageAccountId
        groupIds: [ g ]
      }
    }]
  }
}]

A second, subtler failure surfaced a week later during a routine deploy: a swap moved the staging Service Bus connection string into production because the setting was not marked slot-sticky, and for twenty minutes production wrote acks to the staging topic. No data was lost — the messages sat in staging — but a partner’s 997s were delayed and the incident bridge convened. The fix was to mark every connection and endpoint app setting as a deployment-slot (sticky) setting and to add VNet integration to the staging slot’s own subnet, which had also been missed.

With all four storage endpoints, the matching privatelink DNS zones linked, vnetContentShareEnabled=true, slot-sticky settings, and the slot integrated, the platform stabilised. Post-migration, EDI processing cost dropped by roughly two-thirds (to about ₹21,000), the private-network audit finding closed, and the next peak onboarding ran without a billing spike. The lesson the team wrote on the wall: “On Standard, the runtime’s own storage is a dependency you provision — and a slot is a second environment, not a copy.”

The cutover as a timeline, because the order of failures is the lesson:

Time Symptom Action taken Effect What it should have been
Day 0 App won’t start, restart loop Read app logs (no error) Dead end Check storage reachability first
Day 0 +1h Still looping Found only blob+file PEs Root cause near Provision all four sub-resources
Day 0 +2h Started Added table+queue PE + vnetContentShareEnabled App boots Correct fix
Day 0 +3h DNS resolve fails intermittently One privatelink zone unlinked Flaky Link all four zones to the VNet
Day 7 Acks delayed Swap moved staging SB conn to prod Wrong topic 20 min Mark settings slot-sticky
Day 7 +30m Mitigated Slot-sticky + integrate slot subnet Stable swaps The actual fix is per-slot config

Advantages and disadvantages

The single-tenant model both enables private, cost-efficient B2B and hands you the operational surface Consumption hid. Weigh it honestly:

Advantages (why Standard helps you) Disadvantages (why it bites)
Flat compute billing makes high-fan-out EDI essentially free at the margin You pay for idle compute; low-volume workflows are cheaper on Consumption
Built-in connectors run in-process and honour VNet integration — private targets are reachable Managed connectors still can’t reach private; mixing them is a silent failure
Many workflows per resource; one deployment, scaling, identity unit Resource-level scaling means a noisy workflow can starve quiet ones
Code-like project zip with slots, local dev, func tooling You now own a pipeline, slot settings, and run-history storage
Stateful + stateless side by side: durable core, low-latency edges Choosing wrong (stateful everywhere) taxes throughput and storage
In-process AS2/X12/EDIFACT; integration account no longer needs linking Integration-account SKU is a hard gate (Basic can’t trade)
Managed identity end-to-end: no keys to rotate Slot identities differ; RBAC must be granted per slot or shared
Private endpoints lock both inbound trigger and runtime storage The runtime won’t start if any of four storage sub-resources is unreachable

The model is right for high-volume, private, B2B and event-driven integration where you want code-like CI/CD and durable processing. It is the wrong tool for a handful of runs a day against public SaaS — that’s Consumption’s home. The disadvantages are all manageable, but only if you know they exist before cutover, which is the entire point of the storage and slot sections above.

Hands-on lab

Stand up a Standard logic app, prove it’s a workflow app (not a plain function app), assign a managed identity, and confirm the runtime storage and VNet wiring — all scriptable in Cloud Shell (Bash). Teardown at the end. This uses a WS1 plan; for a zero-cost dry run you can substitute an existing dev subscription’s free credits.

Step 1 — Variables and resource group.

RG=rg-logicapps-lab
LOC=westeurope
PLAN=plan-la-lab
APP=la-lab-$RANDOM            # globally-unique hostname
SA=stlalab$RANDOM            # storage backs run history
az group create -n $RG -l $LOC -o table

Step 2 — Create the storage account and WS1 plan.

az storage account create -n $SA -g $RG -l $LOC \
  --sku Standard_LRS --kind StorageV2 --min-tls-version TLS1_2 \
  --allow-blob-public-access false -o table

az functionapp plan create -n $PLAN -g $RG -l $LOC --sku WS1 --is-linux false -o table

Expected: a plan row with sku.name = WS1.

Step 3 — Create the logic app and confirm its kind.

az logicapp create -n $APP -g $RG -l $LOC \
  --plan $PLAN --storage-account $SA --functions-version 4 -o table

az logicapp show -g $RG -n $APP --query kind -o tsv
# expect: functionapp,workflowapp

If you see functionapp without ,workflowapp, the workflow extension didn’t attach — recreate with --functions-version 4 and the workflow plan.

Step 4 — Assign a managed identity and grant it a data-plane role.

az logicapp identity assign -g $RG -n $APP -o table
PRINCIPAL=$(az logicapp identity show -g $RG -n $APP --query principalId -o tsv)
echo "Identity principalId: $PRINCIPAL"
# Grant it read on its own storage as a demonstration of keyless RBAC
az role assignment create --assignee $PRINCIPAL \
  --role "Storage Blob Data Reader" \
  --scope $(az storage account show -g $RG -n $SA --query id -o tsv) -o table

Step 5 — Force outbound through a VNet (the integration wiring).

az network vnet create -g $RG -n vnet-lab --address-prefixes 10.40.0.0/16 \
  --subnet-name snet-logicapps --subnet-prefixes 10.40.1.0/26 -o table

az network vnet subnet update -g $RG --vnet-name vnet-lab -n snet-logicapps \
  --delegations Microsoft.Web/serverFarms -o table

az functionapp vnet-integration add -g $RG -n $APP --vnet vnet-lab --subnet snet-logicapps -o table
az functionapp config appsettings set -g $RG -n $APP --settings WEBSITE_VNET_ROUTE_ALL=1 -o table

Step 6 — Verify the deployment behaves as designed.

# 1. Workflow app, not a plain function app
az logicapp show -g $RG -n $APP --query kind -o tsv                       # functionapp,workflowapp
# 2. Managed identity present
az logicapp identity show -g $RG -n $APP --query principalId -o tsv       # a GUID
# 3. Outbound routes through the VNet
az functionapp config appsettings list -g $RG -n $APP \
  --query "[?name=='WEBSITE_VNET_ROUTE_ALL'].value" -o tsv                # 1
# 4. VNet integration is attached
az functionapp vnet-integration list -g $RG -n $APP -o table             # one row

Step 7 — Teardown.

az group delete -n $RG --yes --no-wait

What each verification proves and what a failure means:

Check Expected If it fails
kind functionapp,workflowapp Workflow extension not attached; recreate
principalId a GUID Identity not assigned; rerun assign
WEBSITE_VNET_ROUTE_ALL 1 Only RFC1918 routes the VNet; public bypasses
vnet-integration list one row Integration not attached / subnet not delegated

Common mistakes & troubleshooting

These are the failures that actually cost a Standard cutover its weekend. Scan the playbook, then read the detail for whichever row matches. The exact confirm command or blade is in the table — keep it open during go-live.

# Symptom Root cause Confirm (exact cmd / portal path) Fix
1 App won’t start, restart loop, no app error Storage not fully private (missing table/queue PE) az network private-endpoint list -g $RG --query "[?contains(name,'$SA')].name" (expect 4) Add all four PEs + vnetContentShareEnabled=true
2 App starts then fails to mount content vnetContentShareEnabled off on private storage az resource show ... --query properties.vnetContentShareEnabled Set the property true
3 Name resolution fails before networking privatelink DNS zone not linked to VNet az network private-dns link vnet list -g $RG -z privatelink.table.core.windows.net Link all four zones to the VNet
4 Built-in connector can’t reach private SQL/SB WEBSITE_VNET_ROUTE_ALL=0 (only RFC1918 routed) az functionapp config appsettings list --query "[?name=='WEBSITE_VNET_ROUTE_ALL']" Set to 1
5 Connector to private target times out Used a managed connector (egresses from MS) connections.json shows managedApiConnections for a private host Switch to the built-in (service-provider) connector
6 X12/EDIFACT decode: “agreement not found” Integration account is Basic (no agreements) Integration account → SKU in portal Upgrade to Standard SKU
7 B2B operations fail / artifacts missing Integration account in wrong region/subscription Compare IA region vs app region Move IA to same subscription + region
8 Poison interchange hot-loops the consumer Auto-complete on; message re-delivered forever Service Bus → DLQ depth (zero while looping) Turn off auto-complete; abandon on failure → DLQ
9 After swap, prod uses staging creds/endpoint Settings not slot-sticky Compare app settings pre/post swap Mark connection/endpoint settings slot-sticky
10 Slot’s outbound calls leave the VNet Slot has no VNet integration (not inherited) az functionapp vnet-integration list --slot staging (empty) Integrate the slot’s subnet too
11 Run telemetry missing in App Insights APPLICATIONINSIGHTS_CONNECTION_STRING unset/wrong az functionapp config appsettings list --query "[?name=='APPLICATIONINSIGHTS_CONNECTION_STRING']" Set the correct connection string
12 Stateless workflow loses runs on restart Used stateless for a durable orchestration workflow.json kind = Stateless Switch durable paths to Stateful
13 Non-idempotent action runs twice Default exponential retry on a POST Action retry policy in workflow.json Set retry type to none or add idempotency key
14 Managed-identity connector: 401/403 MI lacks data-plane RBAC on target az role assignment list --assignee $PRINCIPAL (empty) Grant the least-privilege data role

Cause 1 — The runtime can’t reach its own storage

The signature is a logic app stuck in a restart loop with no meaningful application error — because the failure is below your code. The runtime needs blob, file, table, and queue reachable. Teams that follow an App Service runbook create only blob and file and the app never starts.

Confirm. Count the private endpoints against the storage account:

az network private-endpoint list -g $RG \
  --query "[?contains(name,'$SA')].name" -o tsv   # expect 4 entries
az resource show -g $RG -n $APP --resource-type "Microsoft.Web/sites" \
  --query properties.vnetContentShareEnabled -o tsv  # expect: true

Fix. Create all four private endpoints, link the four privatelink DNS zones to the VNet, and set vnetContentShareEnabled=true. This is the two-line infrastructure fix from the scenario.

Cause 5 — A managed connector against a private target

A managed connector’s traffic originates from Microsoft’s connector service, not your integrated subnet, so it cannot reach a private endpoint — and the failure is a timeout, not a clear “blocked” message, which sends people hunting in the wrong place.

Confirm. Inspect connections.json — a managedApiConnections entry pointing at a private host is the smoking gun. Fix. Replace it with the built-in (service-provider) connector, which runs in-process and honours VNet integration. For SQL, Service Bus, Blob, Event Hubs, and Cosmos DB, a built-in exists.

Cause 8 — A poison message that won’t die

With auto-complete left on, an exception still completes the message in some flows, or — worse — the message is re-delivered and re-fails forever, pinning your consumer. The DLQ stays empty while the loop runs, which makes it look like nothing is wrong.

Confirm. Watch the Service Bus DLQ depth (zero while the loop runs) and the consumer’s failure rate (climbing). Fix. Turn off auto-complete so an exception abandons the message back to the queue; after max delivery count it lands in the DLQ, where a drain workflow triages it. This converts a hot loop into a parked message.

Best practices

Crisp, production-grade rules, each earned the hard way:

# Rule Why
1 Make the synchronous edges stateless, the orchestration core stateful Low-latency intake without the storage tax on every run
2 Dedicate a same-region storage account per logic app, with retention set Run history is hot-path; isolate I/O and cap cost
3 Provision all four storage sub-resources private + vnetContentShareEnabled The runtime won’t start otherwise
4 Link all four privatelink DNS zones to the VNet Resolution fails before networking does
5 Set WEBSITE_VNET_ROUTE_ALL=1 whenever you need private egress 0 routes only RFC1918; public bypasses the VNet
6 Use built-in connectors for any in-VNet target Managed connectors can’t reach private endpoints
7 Authenticate with managed identity end-to-end; no keys in settings Nothing to rotate; least-privilege data-plane RBAC
8 Choose the integration-account SKU for the job (Standard for trading) Basic can’t hold agreements
9 Route functional acks (997/CONTRL) back to partners on every interchange A bad interchange gets a negative ack, not silence
10 Tune retry per action; set non-idempotent ops to none Avoid duplicate side effects on retry
11 Dead-letter poison messages at the broker, with a drain workflow A poison interchange parks instead of hot-looping
12 Deploy the project zip to a staging slot, smoke-test, then swap Zero-downtime cutover; catch issues in staging
13 Mark connection/endpoint settings slot-sticky; integrate the slot’s subnet A swap won’t leak creds or drop the slot out of the VNet
14 Wire Application Insights and alert on failure rate + DLQ depth Find a degraded path before a partner does

Security notes

The security posture for a private B2B pipeline has several non-negotiables:

Area Control How
Inbound Private trigger endpoint + deny public PE on sites; access-restriction --default-action Deny
Outbound Egress through the VNet, optionally a firewall WEBSITE_VNET_ROUTE_ALL=1; route to Azure Firewall
Identity Managed identity, least-privilege data roles System/user-assigned MI + scoped RBAC
Secrets None in local.settings.json; Key Vault refs @Microsoft.KeyVault(...) resolved by MI
Storage Private endpoints, no public blob access, TLS 1.2+ --allow-blob-public-access false, --min-tls-version TLS1_2
B2B certs AS2 signing/encryption certs in the IA / Key Vault Rotate; never embed in workflows
Transport TLS to partners; AS2 sign + encrypt Per-agreement AS2 settings
Audit Run history + B2B tracking (Premium IA) Retention + Azure Data Explorer tracking

Two specifics that bite: a managed connector that egresses from Microsoft’s network punches a hole in a “private-only” claim, so audit your connections.json for managed entries against private hosts; and a storage account left with allow-blob-public-access true undermines the whole private posture even with private endpoints in place — set it false explicitly. For the egress-control pattern, see Azure Firewall forced tunneling & hub-spoke routing.

Cost & sizing

The bill is driven by allocated compute, not actions — which is the whole reason high-fan-out EDI migrates here. The cost levers:

Cost driver What it is How to control Rough scale
Plan compute The WS/ASP/ASE plan (vCPU + RAM) Right-size WS1→WS3; scale-out bounds The dominant line item
Run-history storage Tables/blobs for stateful runs Retention days; dedicated account Small but grows; prune it
Integration account B2B artifact store (per SKU) Standard for trading; Premium for tracking Fixed per SKU/region
Managed connector calls Per-execution on managed connectors Prefer built-in; reserve managed for SaaS Avoidable for private targets
App Insights ingestion Telemetry volume Sampling; cap daily ingestion Scales with run volume
Private endpoints Per-PE hourly + data Consolidate; needed for private Small per endpoint

Right-sizing the plan by workload shape:

Workload Start at Scale signal Move to
Moderate EDI, small payloads WS1 p95 latency creeping, memory pressure WS2
Heavier EDI, larger transforms WS2 CPU pinned, big-message OOM WS3
Predictable steady load + other apps App Service plan (P1v3) scale up / autoscale
Hard isolation / compliance ASEv3 dedicated capacity

Indicative figures (West Europe; INR ≈ USD × 86, directional only):

Item Approx USD/mo Approx INR/mo Notes
WS1 plan (1 instance) ~$175 ~₹15,000 Baseline compute
WS2 plan (1 instance) ~$350 ~₹30,000 Heavier B2B
Integration account (Standard) ~$1,000 ~₹86,000 Per-region trading SKU
Dedicated storage (LRS, modest) ~$5–20 ~₹400–1,700 Run history; prune to keep low
App Insights (moderate ingest) ~$30–100 ~₹2,600–8,600 Sample to control

The economic crossover versus Consumption: if your interchanges fan into many cheap actions, flat compute wins decisively (the scenario cut ~two-thirds). If you run a handful of times a day, Consumption’s per-action billing is cheaper — don’t pay for an idle WS plan. There is no free tier for production B2B; the Free integration-account SKU and dev plans are for experiments only.

Interview & exam questions

Model answers in 2–4 sentences. These map to AZ-305 (designing integration), AZ-204 (developing solutions), and integration-architect interviews.

1. Why is Logic Apps Standard not just “Consumption with a different SKU”? Standard runs the Logic Apps engine as an extension on the single-tenant Azure Functions host, so one resource hosts many workflows, billing is by allocated compute rather than per action, and built-in connectors run in-process. That gives you VNet integration, private inbound, slots, and code-like deployment — none of which Consumption’s multi-tenant model offers.

2. When does Consumption beat Standard on cost? When the workload runs infrequently — a handful of executions a day. Consumption’s per-action billing is near-free at low volume, whereas Standard charges for an always-allocated plan. Standard wins when high-fan-out workflows fire many cheap actions, because the marginal action is free.

3. Explain stateful vs stateless and when to use each. Stateful persists every run and action I/O to storage, giving durability, inspection, resubmit, and survival across host restarts — use it for orchestration, async, and EDI cores. Stateless runs in memory for low latency and high throughput with no durability — use it for short synchronous validate-and-forward edges, ideally fronting a stateful core.

4. Your Standard logic app won’t start in a locked-down subscription. First thing you check? Whether the runtime can reach its own storage. It needs private endpoints for all four sub-resources — blob, file, table, queue — plus vnetContentShareEnabled=true and the four privatelink DNS zones linked. Teams who provision only blob/file (an App Service habit) hit exactly this.

5. Why can’t a managed connector reach a private SQL database? Managed connectors execute on Microsoft’s multi-tenant connector infrastructure and egress from there, not from your integrated subnet, so they have no route to a private endpoint. Use the built-in (service-provider) SQL connector, which runs in-process on your plan and honours VNet integration.

6. How do you configure outbound vs inbound private connectivity? Outbound uses regional VNet integration through a delegated /26+ subnet, with WEBSITE_VNET_ROUTE_ALL=1 to route all egress (not just RFC1918). Inbound uses a private endpoint on the logic app’s sites group plus an access restriction with --default-action Deny.

7. What changed about integration accounts in Standard? The integration account no longer needs to be linked to the logic app — you reference partners, agreements, and schemas by name, and the old link step is gone. It must still be in the same subscription and region, and you must pick a SKU that supports agreements (Standard), since Basic holds artifacts but can’t trade.

8. Describe the three layers of error handling. Action-level retry policies handle transient dependency blips (tune them, set non-idempotent ops to none); try/catch via Scope + runAfter handles logical failures and compensation within a run; and broker-native dead-lettering handles poison messages across retries, parking a bad interchange instead of hot-looping the consumer.

9. What are the two slot footguns specific to Standard, and how do you avoid them? A swap moves app settings, so unmarked connection/endpoint settings can leak staging credentials into production — mark them slot-sticky. And a slot does not inherit VNet integration, so its outbound calls leave the VNet — integrate the slot’s own subnet.

10. How do you authenticate built-in connectors without secrets? Assign the logic app a managed identity and grant it least-privilege data-plane RBAC on each target (e.g. Service Bus Data Receiver, Storage Blob Data Contributor). The connection references the identity, so local.settings.json holds only endpoints — no keys to rotate.

11. Where do workflow runs and actions appear in Application Insights? Workflow runs surface as requests; individual actions appear as dependencies and traces, each tagged with the workflow and run identifiers via customDimensions. You query traces for failed actions and requests for run-latency percentiles.

12. What integration-account tier do you need for B2B transaction tracking? A Premium-level integration account. Standard supports full agreements and trading, but transaction-level B2B tracking surfaced through Azure Data Explorer is gated behind the Premium tier.

Quick check

  1. Which two storage sub-resources do teams commonly forget to make private, causing a Standard logic app to fail to start?
  2. What single app setting forces all outbound traffic (not just RFC1918) through the VNet?
  3. Why can a managed connector not reach a private-endpoint-only Service Bus namespace?
  4. Which integration-account SKU is the minimum for real X12/EDIFACT trading, and why isn’t Basic enough?
  5. After a slot swap, production starts using the staging connection string. What did you forget to do?

Answers

  1. table and queue. The runtime needs all four of blob, file, table, queue plus vnetContentShareEnabled=true; provisioning only blob/file (an App Service habit) leaves the app unable to start.
  2. WEBSITE_VNET_ROUTE_ALL=1. With it at 0, only RFC1918 destinations route through the VNet and public targets bypass it.
  3. Because a managed connector’s traffic originates from Microsoft’s multi-tenant connector infrastructure, not your integrated subnet, so it has no route to a private endpoint. Use the built-in Service Bus connector instead.
  4. Standard. Basic stores artifacts (schemas, maps) but does not support agreements, and you cannot decode/encode X12/EDIFACT against a partner without an agreement.
  5. You forgot to mark the connection/endpoint app settings as slot-sticky (deployment-slot settings), so the swap moved staging’s value into production.

Glossary

Term Definition
Logic Apps Standard Single-tenant Logic Apps runtime hosted as an extension on the Azure Functions host; kind functionapp,workflowapp.
Workflow One workflow.json definition with a kind of Stateful or Stateless; many per logic app resource.
Stateful workflow A workflow that persists runs and action I/O to external storage, giving durability, inspection, and resubmit.
Stateless workflow A workflow that runs entirely in memory for low latency and high throughput, with no persisted run history by default.
Workflow Standard plan (WS1/WS2/WS3) The elastic single-tenant compute plan that hosts Standard logic apps.
Built-in (service-provider) connector A connector that runs in-process on your plan and honours VNet integration (e.g. Service Bus, SQL, Blob, B2B).
Managed (API) connector A multi-tenant connector that egresses from Microsoft’s infrastructure, bills per call, and cannot reach private endpoints.
Integration account The store of B2B artifacts (partners, agreements, schemas, maps, certificates); in Standard, referenced but not linked.
vnetContentShareEnabled Site property that mounts the app’s content share over the VNet; required when storage is private.
VNet integration Regional outbound connectivity through a delegated /26+ subnet so workflows reach private resources.
Private endpoint A private NIC that brings a resource (the logic app, or each storage sub-resource) onto the VNet.
Managed identity An Entra identity assigned to the logic app for keyless, RBAC-scoped data-plane authentication.
AS2 / X12 / EDIFACT B2B transport (AS2) and EDI dialects (X12, EDIFACT) processed by in-process operations in Standard.
Functional acknowledgment (997 / CONTRL) The receipt returned to a partner indicating acceptance or rejection of an interchange.
Dead-letter queue (DLQ) A broker subqueue where poison messages land after max delivery count, drained by a separate triage workflow.
Deployment slot A swappable copy of the app for zero-downtime deploys; needs its own VNet integration and slot-sticky settings.

Next steps

AzureLogic AppsIntegrationB2BWorkflows
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments