When flight CA-1187 is delayed by three hours, Contoso Airlines always has to do the same things: look up the affected passengers in db-reservas, send them an email and a text message, post the notice in the operations team's channel and open an incident in the support system. None of that is complex business logic; it is gluing systems together. Written as code it would be four integrations with their authentication, their formats and their retries, maintained by Diego Salas. And there is an awkward detail: the people who really know the process are the operations team, and they do not code.
Azure Logic Apps exists for exactly this. It is a workflow engine with more than a thousand ready-made connectors, designed visually and stored as a versionable JSON definition. This lesson teaches you to build that real workflow, to handle its errors, to deploy it across environments — which is where the classic problem lives, the connections — and to know when a logic app is the right answer and when it is not.
Contents
- What a logic app is and how it differs from Functions
- Consumption versus Standard
- Anatomy: trigger, actions and connectors
- The flight delay workflow, step by step
- The underlying JSON definition
- Flow control: conditions, loops, scopes and parallelism
- Error handling: retries, run-after and compensation
- Connections and their authentication
- Monitoring and debugging a failed run
- Integration with the rest of the platform
- Deploying with Bicep and the connections problem
- Cost per action executed
- Logic Apps, Functions or Data Factory
- Common Mistakes and Tips
- Exercises
- Conclusion
- What a logic app is and how it differs from Functions
A logic app is a workflow: a trigger and a sequence of connected actions, where the output of each step feeds the next. You compile nothing and manage no dependencies.
| Azure Functions | Azure Logic Apps | |
|---|---|---|
| How it is built | By writing code | Visual designer (+ JSON) |
| Who maintains it | Developers | Developers and business people |
| Strong point | Custom logic and transformation | Connecting existing systems |
| Integrating with SaaS | Write the HTTP client | Ready-made connector |
| Cost | Per execution and GB-s | Per action executed |
| Debugging | Logs and traces | Visual history with the data from each step |
| Where it shines | Computation, algorithms, complex transformation | Orchestration across services and human waits |
The rule is simple: if the problem is "do something complicated", Functions; if the problem is "talk to six different systems", Logic Apps. And they do not compete: Contoso's workflow will call a function from 06-03 for the part that genuinely requires code.
- Consumption versus Standard
| Consumption | Standard | |
|---|---|---|
| Engine | Shared multi-tenant | Dedicated, on the Functions runtime |
| Price | Per action executed | Per hosting plan (fixed) |
| Workflows per resource | One | Several in the same app |
| Virtual network integration | No (requires an ISE) | Yes, with private endpoints |
| Local run and debugging | No | Yes, with VS Code |
| Built-in connectors | Few | Many more, with no per-action cost |
| When | Occasional, simple workflows | Production, high volume, private networking |
Contoso chooses Standard for logic-contoso-retrasos-pro for two concrete reasons: it needs to reach pe-sql-reservas over a private endpoint, and at high volume a fixed price is more predictable than paying per action when a single delay can trigger hundreds of actions.
- Anatomy: trigger, actions and connectors
- Trigger: what starts the workflow. It can be an HTTP request, a schedule, or a connector that polls a system ("when an email arrives", "when a row is created").
- Action: every subsequent step. It queries data, calls an API, sends a message, decides, iterates.
- Built-in connector: it runs inside the engine. Fast, with no per-action cost on the Standard tier: HTTP, Service Bus, Azure Functions, SQL Server, flow control.
- Managed connector: a Microsoft-hosted wrapper around an external service: Office 365, Salesforce, SAP, Twilio, ServiceNow, Slack. It is billed per action and requires a connection with its credentials.
The real value of Logic Apps is that catalog of more than a thousand connectors. Integrating ServiceNow by hand is days of work — authentication, schema, retries, pagination — and here it is dragging in an action. That is the whole argument, and it is a strong one.
- The flight delay workflow, step by step
graph TD
T["HTTP trigger:<br/>POST /retraso {flight, minutes}"] --> V{"minutes > 60?"}
V -->|No| END["Log it and finish"]
V -->|Yes| SQL["SQL: affected passengers<br/>from db-reservas"]
SQL --> FE["For each passenger (in parallel)"]
FE --> MAIL["Office 365: email"]
FE --> SMS["Twilio: text message"]
SQL --> OPS["Teams: notice to the<br/>operations channel"]
SQL --> INC["ServiceNow: open an incident"]
SQL --> FUN["Function CalcularCompensacion<br/>(06-03)"]
FUN --> INC
The steps, in order:
- The "When an HTTP request is received" trigger. It generates a URL with a signature; the operations system posts
{ "flight": "CA-1187", "minutes": 180, "date": "2026-08-15" }to it. A JSON schema for the payload is defined so that the fields appear as selectable tokens in the following steps. - Condition: if the delay does not exceed 60 minutes, there is no obligation to notify. It is logged and the workflow ends.
- The SQL "Execute a query" action against
db-reservas, with the flight number and the date as parameters — never concatenated into the query text, which would be SQL injection — returning the record locator, name, email and phone number. - Calling the
CalcularCompensacionfunction from 06-03 with the delay and the route. That calculation depends on European regulation, has its special cases and is code: it is not done with visual actions. - A
for eachloop over the passengers, in parallel, with the email (Office 365) and the text message (Twilio) sent inside it. - A notice to the operations channel in Teams, with the summary and the number of people affected.
- Opening the incident in ServiceNow with the calculated compensation amount.
- The underlying JSON definition
The designer is a view over a file. This matters: the logic app is versionable code that lives in contoso-infra and goes through review like any other change (05-02).
{
"definition": {
"triggers": {
"DelayRequest": {
"type": "Request", "kind": "Http",
"inputs": { "schema": { "type": "object", "properties": {
"flight": { "type": "string" },
"minutes": { "type": "integer" },
"date": { "type": "string" } } } }
}
},
"actions": {
"IfOverOneHour": {
"type": "If",
"expression": { "greater": [ "@triggerBody()?['minutes']", 60 ] },
"actions": {
"GetPassengers": {
"type": "ApiConnection",
"inputs": {
"host": { "connection": { "name": "@parameters('$connections')['sql']['connectionId']" } },
"method": "post", "path": "/datasets/default/query/sql",
"body": {
"query": "SELECT localizador, nombre, correo, telefono FROM Reservas WHERE numeroVuelo=@flight AND fecha=@date",
"parameters": { "flight": "@triggerBody()?['flight']",
"date": "@triggerBody()?['date']" } }
},
"runAfter": {}
},
"ForEachPassenger": {
"type": "Foreach",
"foreach": "@body('GetPassengers')?['resultsets']['Table1']",
"runtimeConfiguration": { "concurrency": { "repetitions": 20 } },
"actions": { "SendEmail": { "type": "ApiConnection", "inputs": { } } },
"runAfter": { "GetPassengers": [ "Succeeded" ] }
}
}
}
}
}
}Three pieces worth recognizing:
@triggerBody()?['minutes']is the expression language. The?is safe access: if the field is not there, it returns null instead of breaking the workflow. Always use it with external data.runAfteris what defines the real order of execution, not the visual position. A step with an emptyrunAfterstarts as soon as the branch begins; two actions with the samerunAfterrun in parallel.concurrency.repetitionslimits how many iterations of the loop run at once. Without that limit, a flight with 300 passengers fires 300 simultaneous calls to Twilio and triggers throttling.
- Flow control: conditions, loops, scopes and parallelism
- Condition (
If) with two branches, andSwitchwhen there are several discrete cases, such as the incident type. For each: iterates over a collection, in parallel by default with 20 simultaneous repetitions. If order matters, you have to force concurrency to 1.Until: repeats until a condition is met or a counter runs out. It is the polling pattern — "check the refund status every 5 minutes until it is approved or 2 hours pass". Always set a limit on iterations and time, or you have a billable infinite loop.- Scope (
Scope): groups actions into a block that has its own aggregate status. It is the basis of the error handling in the next section: it works like atry. - Parallel branches: two actions with the same
runAfterrun at the same time. Contoso uses this so that the Teams notice and the ServiceNow incident do not wait for the passenger loop.
- Error handling: retries, run-after and compensation
Three layers, from the most automatic to the most manual.
Retry policy. Every action has one and by default retries 4 times with exponential backoff on transient errors (429 and 5xx). It is adjusted per action:
"retryPolicy": { "type": "exponential", "count": 5,
"interval": "PT10S", "maximumInterval": "PT1H" }With "type": "none" it is disabled, and there is one case where it has to be disabled: when the action is not idempotent. Retrying a charge on the payment gateway bills twice. It is the same lesson as 06-03, here in visual form.
Run-after actions. runAfter can be conditioned on Failed, TimedOut or Skipped, not only on Succeeded. That is the catch:
"NotifyIntegrationFailure": {
"type": "ApiConnection",
"runAfter": { "NotificationsScope": [ "Failed", "TimedOut" ] },
"inputs": { }
}Combined with a scope, the try/catch pattern looks like this: a NotificationsScope scope with the sends inside it, and a subsequent action that runs only if the scope failed, alerting the team with @result('NotificationsScope'), which returns the detail of exactly which action blew up.
Compensation. There are no distributed transactions: if the email was sent and the text message failed, the email cannot be "undone". The pattern is to add actions that correct — mark the notice as partial, enqueue a resend, open an incident for manual review. Design it explicitly, because the workflow is going to fail halfway sooner or later.
- Connections and their authentication
A connection is an Azure resource, separate from the workflow, that stores how to authenticate against a system. And they come in three kinds:
| Kind | Example | Risk |
|---|---|---|
| Managed identity | SQL Server, Blob, Key Vault, Service Bus | None: no secrets, and it is the right option |
| Service principal | Azure APIs with specific permissions | A secret to rotate |
| Delegated OAuth | Office 365, Teams, Salesforce | Tied to a person: if they leave, the workflow dies |
Contoso uses a managed identity whenever the connector supports one: id-contoso-api-pro reaches db-reservas and kv-contoso-pro with no credentials. For connectors that demand delegated OAuth, the team's rule is to authorize them with a domain service account — [email protected] — and never with Marta's or Diego's personal account. It is a mistake you pay for months later, when somebody changes role and the workflows start failing with 401.
- Monitoring and debugging a failed run
The run history is the best diagnostic tool in the entire module. Every run shows the workflow drawn out with each step in green or red and, when you click a step, the exact inputs and outputs of that specific run: the JSON that went in, the JSON that came out and the literal error.
The practical debugging process: open the failed run, find the first step in red — the ones after it are usually consequences — read its inputs to see what data it actually received, and compare that with what you expected. Most failures are about the shape of the data: a null field that was assumed to be present, a date in a different format, an empty collection. Once the problem in the source system is fixed, resubmit relaunches that same run with the same input data, without having to reproduce the original event.
Round it out by sending diagnostics to log-contoso-pro so you can query them with KQL and create alerts on the number of failed runs (07-01 and 07-02).
- Integration with the rest of the platform
This is where Logic Apps stops being a standalone tool:
- From an Azure Monitor alert (07-01): an action group can invoke the HTTP trigger's URL. That way, "the latency of
app-contoso-reservas-proexceeds 2 s" stops being an email and becomes a workflow that opens the incident, notifies the channel and attaches the link to the dashboard. - From Defender for Cloud (04-05): its workflow automations fire logic apps on a security alert, to isolate a resource or notify the on-call owner.
- From Event Grid (06-05): reacting to platform events without writing anything.
- Calling a function from 06-03 with the built-in Azure Functions connector, for the logic that genuinely is code. This combination is the good one: Logic Apps orchestrates, Functions computes.
- Deploying with Bicep and the connections problem
The workflow is deployed like any other resource from contoso-infra, with the 05-06 infrastructure pipeline:
resource delayWorkflow 'Microsoft.Logic/workflows@2019-05-01' = {
name: 'logic-contoso-retrasos-${environment}'
location: location
identity: { type: 'UserAssigned', userAssignedIdentities: { '${identityId}': {} } }
tags: commonTags
properties: {
state: 'Enabled'
definition: json(loadTextContent('flujos/retrasos.json'))
parameters: {
'$connections': { value: { sql: {
connectionId: sqlConnection.id
connectionName: sqlConnection.name
id: subscriptionResourceId('Microsoft.Web/locations/managedApis',
location, 'sql') } } }
}
}
}And here is the classic problem. The workflow definition is promoted cleanly across environments, but the connections are not: each environment has its own, with different identifiers, and the delegated OAuth ones need a human to click "Authorize" in the portal the first time. If they are not kept separate, the production deployment drags the development connection along and the workflow ends up querying db-reservas in the wrong environment. The discipline that prevents this:
- The workflow definition never carries hard-coded connection identifiers: they go in the
$connectionsparameter. - Connections are declared as separate resources, per environment, with their
.bicepparam. - Managed identity ones are fully automated; OAuth ones are authorized once with the domain service account, and that manual step is documented.
- Cost per action executed
On the Consumption tier you pay for every action executed, and the subtlety is that a loop's iterations count separately. Do the arithmetic for Contoso's workflow: a flight with 250 passengers runs 2 actions per passenger, so 500, plus a dozen for the rest. Five delays a day is 2,500 actions daily. At cents per thousand actions it looks like nothing, but the pattern that blows up the bill without warning is always the same: a polling trigger every minute — 43,200 checks a month even when nothing happens — or a badly bounded Until, or a nested loop that multiplies.
The measures: use notification triggers instead of polling when the system allows it (webhook or Event Grid), space the polling out to what is genuinely needed, bound every Until with a counter and a maximum time, and at high volumes move to the Standard tier, where the cost is a fixed plan and built-in connectors are not billed per action.
- Logic Apps, Functions or Data Factory
| Logic Apps | Azure Functions | Data Factory (03-06) | |
|---|---|---|---|
| Purpose | Orchestrate and integrate systems | Run custom logic | Move and transform data at scale |
| Unit of work | A business event | An event or request | A batch of millions of rows |
| Construction | Visual designer + JSON | Code | Pipelines and data flows |
| SaaS connectors | More than a thousand | The ones you write | Many, data-oriented |
| Human waits | Yes, natively | With Durable Functions | No |
| Contoso's case | Notifying about a flight delay | Generating the boarding pass PDF | Loading stlagocontosopro every night |
The boundary with Data Factory is settled with one question: does the workflow handle one case or a set? Notifying the passengers on one flight is a case: Logic Apps. Loading 40 million boarding records into the bronce layer is a set: Data Factory.
Common Mistakes and Tips
- Authorizing OAuth connections with a personal account. The day that person changes role, the workflows fail with 401. Use a domain service account.
- Hard-coding connection identifiers in the definition. It breaks promotion across environments. Use the
$connectionsparameter. - Concatenating values into a SQL query. Textbook SQL injection. Use parameters.
- Leaving
for eachat its default concurrency when it calls a rate-limited API. 300 simultaneous calls cause throttling and cascading failures. - Retrying non-idempotent actions. A retried charge is billed twice. Disable the policy on those steps.
- An
Untilwith no iteration and time limits. An infinite loop that also bills. - Polling every minute out of convenience. It is the number one cause of surprise bills on the Consumption tier.
- Tip: name your actions after what they do (
GetAffectedPassengers), not with the default name. Expressions reference that name and renaming it later breaks them. - Tip: if the workflow has more than 30 actions or a lot of conditional logic, the logic belongs in a function. The visual designer stops helping once the workflow looks like a plate of spaghetti.
Exercises
Exercise 1. Contoso Miles needs a monthly workflow that calculates each member's loyalty tier, sends an email to those who move up a tier and posts the summary in the commercial team's channel.
- Choose the tier (Consumption or Standard) and the trigger, and justify your choice.
- List the actions with their flow control, saying where you would call a function and why.
- State the mandatory tags and estimate the order of magnitude of the cost if there are 80,000 members and 3,000 move up a tier.
Exercise 2. This workflow is failing: the UploadPassport action uploads a document to storage, UpdateBooking marks the check-in in db-reservas, and SendConfirmation notifies the passenger. Yesterday UpdateBooking failed with a timeout, and today there are uploaded passports for bookings that show no check-in.
- Explain why it failed, including what the retries did.
- Redesign the workflow with scopes, run-after and compensation.
- How does this differ from a transaction, and what guarantee can you genuinely give?
Exercise 3. For each case choose Logic Apps, Functions or Data Factory: (a) on receiving an email from a supplier with a CSV attachment, validate it and open an incident if it has errors; (b) recalculate the fares for 12 million origin-destination combinations every night; (c) when a payment is confirmed, generate the invoice PDF with the tax rules of seven countries.
Solutions
Solution 1:
- Consumption with a monthly schedule trigger: it runs once a month, it does not need a virtual network and the Standard tier's fixed price is not justified for twelve runs a year.
- Schedule → call the
CalcularNivelesfunction with the batch of members →for eachover those who move up a tier, with limited concurrency, sending the email inside → a Teams notice with the summary. The tier calculation goes in a function because it is business rules with thresholds, special cases and validity periods: that is code, it is tested with unit tests and it is not maintained with visual actions. On top of that, processing 80,000 members step by step in the designer would be desperately slow and horribly expensive in actions. entorno,proyecto=contoso-millas,centro-coste=CC-2077andpropietario. The cost depends on the key design decision: if the evaluation happens inside the function, you are billed for on the order of 3,000 iterations plus a dozen actions, that is, a few thousand actions a month: cents. If instead you iterated over all 80,000 members in the workflow with two actions each, it would be 160,000 actions a month. The lesson is right there: the loop in the wrong place multiplies the bill by fifty.
Solution 2:
UpdateBookingtimed out againstdb-reservas. The default retry policy retried it four times with exponential backoff and, as it kept failing, the action ended upFailed; sinceSendConfirmationhadrunAfter: Succeeded, it never ran and the workflow finished as failed. ButUploadPassporthad already completed, and nothing reverted it: hence the orphaned documents.- Wrap
UploadPassportandUpdateBookingin aCheckInScopescope. Add an action withrunAfter: { CheckInScope: ["Failed","TimedOut"] }that runs the compensation: mark the uploaded document as pending reconciliation, enqueue a deferred retry oncola-emision-tarjetasand open an incident with@result('CheckInScope').SendConfirmationstays withrunAfter: Succeededon the scope, so that the passenger only gets the confirmation if everything went well. - A transaction would give you atomicity: all or nothing, with automatic rollback. There is none here, because the systems are independent and share no coordinator. What you can guarantee is eventual consistency with compensation: every partial effect is recorded and there is an explicit path that corrects it or escalates it to a human. The practical difference is that a window exists in which the system is inconsistent, and the design has to accept it and make it visible instead of pretending it is not there.
Solution 3: (a) Logic Apps: the email trigger, the attachment and opening the incident are three ready-made connectors, and the workflow is pure integration. (b) Data Factory: it is a massive set processed in batch; doing it with workflow actions would be absurd in both cost and time. (c) Functions: the tax rules of seven countries plus PDF generation is complex logic and testable code; at most, a logic app invokes that function as part of a larger workflow.
Conclusion
You know what a logic app is and how it differs from a function: visual workflow versus code, integrating versus computing, cost per action versus cost per execution, and that the good combination is Logic Apps orchestrating and Functions computing. You can tell Consumption from Standard by engine, price, virtual network, number of workflows and local execution, and you know why Contoso chose Standard for logic-contoso-retrasos-pro. You have mastered the anatomy — trigger, actions, built-in versus managed connectors — and you understand that the catalog of more than a thousand connectors is the service's real argument.
You have built Contoso's real workflow from end to end: an HTTP request with a schema, a condition on the 60 minutes, a parameterized query against db-reservas, a call to a function for the compensation, a loop over the passengers with limited concurrency, a notice to the operations channel and an incident in the support system. And you have read its JSON definition, which is what turns the designer into something versionable in contoso-infra: @triggerBody()?[...] with safe access, runAfter as the real order of execution and explicit control of parallelism. You handle the complete flow control — conditions, Switch, for each, a bounded Until, scopes and parallel branches — and the three layers of error handling: retry policies (disabled where the action is not idempotent), run-after actions as a catch, and compensation, because there are no transactions here. You know how to authenticate with a managed identity wherever you can and with a domain service account when delegated OAuth is unavoidable, how to debug by reading real inputs and outputs in the run history and how to resubmit, and how to fire workflows from Azure Monitor, Defender for Cloud and Event Grid. And you take away two concrete warnings: the connections problem when promoting across environments, with its discipline of parameters and separate resources, and the cost per action, which explodes with minute-by-minute polling and badly placed loops.
Something deeper remains, and it underpins everything above. Every time Contoso confirms a booking, six things happen today inline, within the same request: the payment is taken, availability is updated, the boarding pass is issued, the miles are credited, billing is notified and the confirmation is sent. If the miles service is down, the purchase fails. It is a coupled architecture, and neither Functions nor Logic Apps fixes it on their own: what is needed is for the components to stop calling each other directly and start exchanging messages and events. The next lesson, Messaging and Events: Service Bus, Event Grid and Event Hubs, tackles that head-on: the distinction between message and event that almost everybody confuses, the three services with their decision table, queues and topics with filters and a dead-letter queue, event distribution with CloudEvents, massive telemetry ingestion with Capture into the Data Lake, and the complete design of Contoso's flow with the consequences you can already guess: at-least-once delivery, ordering and idempotency.
Azure Course
Module 1: Introduction to Azure
- What Is Azure?
- Service Models, Regions and Availability Zones
- Creating and Setting Up Your Azure Account
- A Tour of the Azure Portal
- Azure Resource Manager: Subscriptions, Resource Groups and Tags
- Azure CLI, PowerShell and Cloud Shell
Module 2: Core Azure Services
- Azure Virtual Machines
- Compute Scaling and High Availability
- Azure App Service
- Azure Storage: Blobs, Files, Queues and Tables
- Azure Networking: Virtual Networks, Subnets and NSGs
- Hybrid Connectivity and Global Delivery
Module 3: Azure Databases
- Choosing the Right Data Service
- Azure SQL Database
- Azure Cosmos DB
- Azure Database for MySQL
- Azure Database for PostgreSQL
- Data Analytics: Data Lake, Data Factory and Synapse
Module 4: Security in Azure
- Microsoft Entra ID and Identity Management
- RBAC and Managed Identities
- Azure Key Vault
- DDoS Protection and Web Application Firewall
- Microsoft Defender for Cloud
- Governance and Compliance with Azure Policy
Module 5: Azure DevOps
- Introduction to Azure DevOps
- Azure Repos
- Azure Pipelines: Continuous Integration
- Continuous Deployment with Environments and Approvals
- Azure Artifacts
- Infrastructure as Code with Bicep
Module 6: Advanced Azure Services
- Containers in Azure: Container Registry and Container Apps
- Azure Kubernetes Service (AKS)
- Azure Functions
- Azure Logic Apps
- Messaging and Events: Service Bus, Event Grid and Event Hubs
- Azure AI Services
Module 7: Monitoring and Management
- Azure Monitor: Metrics, Alerts and Dashboards
- Log Analytics and KQL Queries
- Application Insights
- Azure Automation and Runbooks
- Backup and Disaster Recovery
Module 8: Cost Management and Optimization
- Pricing Calculator and Cost Estimation
- Azure Cost Management: Analysis, Budgets and Alerts
- Reservations, Savings Plans and Azure Hybrid Benefit
- Azure Advisor
- Optimization Strategies and FinOps Culture
