Adds a single action to an existing Logic App workflow, defined as a raw JSON body (
azurerm_logic_app_action_custom). Targetshashicorp/azurerm ~> 4.0.
- 🧩 Adds one action to an existing Logic App workflow as a keystone resource named
this. - 🚪 The escape hatch for any action the provider has no typed resource for — a connector call, a condition, a scope, a loop.
- 📄 Takes the raw Logic Apps action
bodyas JSON, so anything the definition language can express is available. - 🔗 Emits
nameso other actions can order against it viarun_afterwithout hard-coding a string. ⚠️ States plainly what is not checked: Terraform checks only that the body is well-formed JSON —StringIsJSONruns at plan — and validates nothing about its meaning.- 🧭 Points at
terraform-azurerm-logic-app-action-httpwhere a typed alternative exists.
💡 Why it matters: This module trades type safety for reach. That is the right trade for a connector action the provider does not model — and the wrong one for an HTTP call, which has a typed module with real enum checking. The documentation's job here is to make sure you know which situation you are in.
If this module saves you time, please consider supporting its continued development:
- ⭐ Star the repository on GitHub.
- 🤝 Connect on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
flowchart LR
wf["terraform-azurerm-logic-app-workflow"]
trig["terraform-azurerm-logic-app-trigger-recurrence or -http-request or -custom"]
this["terraform-azurerm-logic-app-action-custom"]
act["azurerm_logic_app_action_custom"]
http["terraform-azurerm-logic-app-action-http"]
ia["terraform-azurerm-logic-app-integration-account"]
wf -->|"logic_app_id"| this
wf -->|"logic_app_id"| trig
wf -->|"logic_app_id"| http
trig -->|"starts the run"| act
this -->|"creates"| act
act -->|"name referenced by run_after"| http
ia -->|"maps and schemas the body may reference"| act
classDef me fill:#0078D4,stroke:#004578,color:#fff;
classDef keystone fill:#004578,stroke:#001f3f,color:#fff;
classDef sib fill:#eef2f7,stroke:#b8c4d0,color:#1b1b1b;
class this me;
class act keystone;
class wf,trig,http,ia sib;
flowchart TB
name["name: unique across all actions, force-new"]
wf["logic_app_id: force-new"]
body["body: opaque JSON action definition"]
guard["no plan-time validation of the document"]
this["terraform-azurerm-logic-app-action-custom"]
act["azurerm_logic_app_action_custom.this"]
order["other actions order against this name via run_after"]
out["outputs: id, name, logic_app_id"]
name -->|"identity and ordering handle"| this
wf -->|"parent workflow"| this
body -->|"the action itself"| this
guard -->|"prefer a typed action module"| body
this -->|"creates"| act
act -->|"referenced by"| order
act -->|"emits"| out
classDef me fill:#0078D4,stroke:#004578,color:#fff;
classDef keystone fill:#004578,stroke:#001f3f,color:#fff;
classDef sib fill:#eef2f7,stroke:#b8c4d0,color:#1b1b1b;
class this me;
class act keystone;
class name,wf,body,guard,order,out sib;
Resource inventory
| Resource | Count | Role |
|---|---|---|
azurerm_logic_app_action_custom.this |
1 | The keystone action, with its timeouts block. |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/azurerm |
~> 4.0 |
| Provider block | None in this module — the caller configures provider "azurerm" { features {} }, auth, and subscription. |
Schema notes that bite (verified against the live provider schema):
nameandlogic_app_idare force-new.namemust be unique across every action in the workflow, and the provider cannot check that — each action is a separate resource, so Terraform sees only this one.- The
idis unique to Terraform: it is the workflow ID with/actions/<name>appended and matches no identifier the Azure APIs return for the action. Importing means composing it by hand. - Terraform validates the body only as JSON at plan (
StringIsJSON), and that check has two gaps: an EMPTY string and any non-object JSON both pass plan and fail at apply. A malformed action, a wrongtype, or a reference to a non-existent connection all fail at apply or at run time, never at plan. - Actions and triggers written as separate resources against one workflow update the same underlying definition, and the provider DOES serialise them -- it takes a per-workflow mutex. That mutex is in-process only, so it protects components within a single apply and nothing else: two concurrent applies, or an apply overlapping a portal edit, silently lose one side's changes, because every write is a read-modify-write of the whole workflow — a workflow with many action resources can produce concurrent-update conflicts on apply.
- A workflow whose definition is also managed inline by
terraform-azurerm-logic-app-workflowwill fight these resources. Choose one mechanism per workflow. - This resource type has no
tagssurface.
Logic App Contributorat the workflow's resource group, or a custom role coveringMicrosoft.Logic/workflows/write— an action is written by updating the parent workflow definition, so the permission is on the workflow, not on a child resource.- Note the consequence: anyone who can add an action can change what the workflow does using whatever identity and connections it already holds. Treat write access to a workflow as equivalent to that workflow's effective permissions.
- An existing Logic App (Consumption) workflow.
- Any API connection the action body references already created and authorized — a connector's consent flow is interactive for many connectors and is not something Terraform performs.
- The
Microsoft.Logicresource provider registered on the target subscription. - The caller configures the
provider "azurerm" { features {} }block, auth, and subscription.
terraform-azurerm-logic-app-action-custom/
├── providers.tf # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
├── variables.tf # name, logic_app_id, body, timeouts tail
├── main.tf # keystone azurerm_logic_app_action_custom.this; dynamic timeouts
├── outputs.tf # id, name, logic_app_id
├── README.md # this document
├── SCOPE.md # cross-module contract
├── LICENSE # MIT
└── .gitignore # canonical library ignore set
provider "azurerm" {
features {}
}
module "notify_action" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-logic-app-action-custom.git?ref=v1.0.0"
name = "Compose_Notification"
logic_app_id = module.order_workflow.id
body = jsonencode({
type = "Compose"
inputs = {
subject = "Order received"
body = "@{triggerBody()}"
}
})
}ℹ️ The caller owns the provider, its authentication, and the mandatory
features {}block. This module never declares them.
Consumes
| Input | Type | Source module |
|---|---|---|
logic_app_id |
string |
terraform-azurerm-logic-app-workflow (id) |
body |
string (JSON) |
caller (jsonencode() or file()) |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
The Terraform-composed action ID (first) | audit inventories |
name |
Action name | terraform-azurerm-logic-app-action-http (run_after[*].action_name), and any other action ordering against this one |
logic_app_id |
The parent workflow | composition wiring |
The examples below reference existing resources by ID or name rather than creating them; this module owns only its own resource. Those references are declared inputs:
variable "get_order_name" {
description = "name of an existing get order that these examples reference but do not create."
type = string
}
variable "workflow_id" {
description = "id of an existing workflow that these examples reference but do not create."
type = string
}1 · A minimal Compose action
module "compose" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-logic-app-action-custom.git?ref=v1.0.0"
name = "Compose_Message"
logic_app_id = var.workflow_id
body = jsonencode({
type = "Compose"
inputs = "@{triggerBody()?['orderId']}"
})
}💡
jsonencode({...})rather than a heredoc means a malformed document fails at plan, and the HCL stays readable.
2 · Loading the body from a file
body = file("${path.module}/actions/enrich-order.json")💡 For anything beyond a few lines this is the better form: the action lives in a real
.jsonfile that an editor can validate and a reviewer can read as a diff.
3 · Ordering with `runAfter` inside the body
body = jsonencode({
type = "Compose"
inputs = "@{body('Get_Order')}"
runAfter = {
Get_Order = ["Succeeded"]
}
})ℹ️ A custom action expresses ordering inside its own
runAftermap, because this resource has no typedrun_afterblock — unliketerraform-azurerm-logic-app-action-http. The key is the preceding action's name.
4 · Wiring the predecessor name instead of typing it
body = jsonencode({
type = "Compose"
inputs = "@{body('${var.get_order_name}')}"
runAfter = {
(var.get_order_name) = ["Succeeded"]
}
})💡 Referencing the upstream module's
nameoutput means a rename surfaces as a plan diff rather than as a workflow that fails at run time. Note the parenthesised key — HCL needs it for a computed map key.
5 · A condition (If) action
body = jsonencode({
type = "If"
expression = {
and = [{
greater = ["@int(triggerBody()?['amount'])", 1000]
}]
}
actions = {}
else = { actions = {} }
runAfter = {}
})ℹ️ Control-flow actions —
If,Switch,Foreach,Until,Scope— have no typed resources at all, which is the main reason this module exists.
6 · A Foreach loop with a nested action
body = jsonencode({
type = "Foreach"
foreach = "@triggerBody()?['lines']"
actions = {
Compose_Line = {
type = "Compose"
inputs = "@{items('Foreach_Line')?['sku']}"
}
}
runAfter = {}
})
⚠️ Actions nested inside aForeachorScopeare part of this action's body, not separate resources. Do not also create them with their own module instances — they would be duplicated.
7 · A connector action referencing an API connection
body = jsonencode({
type = "ApiConnection"
inputs = {
host = {
connection = {
name = "@parameters('$connections')['office365']['connectionId']"
}
}
method = "post"
path = "/v2/Mail"
body = {
To = "ops@example.com"
Subject = "Order received"
}
}
runAfter = {}
})
⚠️ The connection must already exist and be authorized. Many connectors require an interactive consent flow that Terraform cannot perform, so a first apply can produce an action that exists and fails when it runs.
8 · Error handling: an action that runs only on failure
body = jsonencode({
type = "Compose"
inputs = "Order processing failed: @{result('Process_Order')}"
runAfter = {
Process_Order = ["Failed", "TimedOut"]
}
})💡 This is a Logic App's catch block. Listing several results means "any of these", unlike the typed HTTP action's
run_afterblock, where several predecessors must all reach their stated result.
9 · Keeping secrets out of the body
# ❌ The body is stored in the workflow definition, readable by anyone with read
# access to the Logic App.
body = jsonencode({
type = "Http"
inputs = {
headers = { Authorization = "Bearer eyJhbGciOi..." }
}
})# ✅ Reference a workflow parameter, or use a connection that holds the credential.
body = jsonencode({
type = "Http"
inputs = {
headers = { Authorization = "@parameters('apiToken')" }
}
})🔒 The action body is configuration, not a secret store. Put the value in a workflow parameter sourced from Key Vault, or use an API connection — and note that a typed HTTP action (
terraform-azurerm-logic-app-action-http) documents this same constraint with a real schema behind it.
10 · Several actions from a keyed map
locals {
actions = {
Compose_Header = { type = "Compose", inputs = "@{triggerBody()?['header']}" }
Compose_Lines = { type = "Compose", inputs = "@{triggerBody()?['lines']}" }
}
}
module "actions" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-logic-app-action-custom.git?ref=v1.0.0"
for_each = local.actions
name = each.key
logic_app_id = var.workflow_id
body = jsonencode(each.value)
}
⚠️ Every one of these updates the same workflow definition, and the provider DOES serialise them -- it takes a per-workflow mutex. That mutex is in-process only, so it protects components within a single apply and nothing else: two concurrent applies, or an apply overlapping a portal edit, silently lose one side's changes, because every write is a read-modify-write of the whole workflow. On a workflow with many actions this can produce concurrent-update conflicts; see example 11.
11 · Serialising updates to avoid conflicts
module "second_action" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-logic-app-action-custom.git?ref=v1.0.0"
name = "Compose_Second"
logic_app_id = var.workflow_id
body = jsonencode({ type = "Compose", inputs = "second" })
# Forces Terraform to write one action at a time.
depends_on = [module.first_action]
}💡 A
depends_onchain between action modules is the practical fix for concurrent-update errors on a busy workflow. Ordering the actions by theirrun_afterdependencies usually produces the chain naturally.
12 · When to use the typed HTTP action instead
# ❌ Reaching for the escape hatch when a typed module exists.
module "call_api" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-logic-app-action-custom.git?ref=v1.0.0"
logic_app_id = var.logic_app_id
name = "Call_API"
body = jsonencode({ type = "Http", inputs = { method = "POST", uri = "http://api.example.com" } })
}# ✅ Typed: validated method enum, HTTPS-enforced URI, typed run_after.
module "call_api" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-logic-app-action-http.git?ref=v1.0.0"
name = "Call_API"
logic_app_id = var.workflow_id
method = "POST"
uri = "https://api.example.com/orders"
}💡 The typed module would have rejected that
http://URI at plan. Use this module for what the provider does not model, not as a general-purpose action builder.
13 · Custom timeouts
timeouts = {
create = "30m"
read = "5m"
update = "30m"
delete = "30m"
}ℹ️ The defaults are ample. Raise
createonly if workflow updates in your tenant are consistently slow — which on a workflow with many action resources is usually the concurrency issue from example 11 rather than latency.
14 · Importing an existing action
terraform import 'module.compose.azurerm_logic_app_action_custom.this' \
"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-integration/providers/Microsoft.Logic/workflows/wf-orders/actions/Compose_Message"
⚠️ That ID is composed by hand — it is the workflow's Resource ID with/actions/<name>appended, and no Azure API returns it. Get the workflow ID right and append the action name exactly as it appears in the definition.
15 · 🏗️ End-to-end composition
provider "azurerm" {
features {}
}
# 1 · The resource group.
module "integration_rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-integration-eastus2"
location = "eastus2"
}
# 2 · The workflow. Its definition is left empty here, because the trigger and
# actions below own it — do not also define them inline.
module "order_workflow" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-logic-app-workflow.git?ref=v1.0.0"
name = "wf-orders"
resource_group_name = module.integration_rg.name
location = module.integration_rg.location
}
# 3 · A request trigger. Its callback URL is a bearer credential — see that module.
module "order_trigger" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-logic-app-trigger-http-request.git?ref=v1.0.0"
name = "When_An_Order_Arrives"
logic_app_id = module.order_workflow.id
schema = jsonencode({
type = "object"
required = ["orderId", "amount"]
properties = {
orderId = { type = "string" }
amount = { type = "number" }
}
})
}
# 4 · A typed HTTP action — the first step, so no run_after.
module "fetch_customer" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-logic-app-action-http.git?ref=v1.0.0"
name = "Fetch_Customer"
logic_app_id = module.order_workflow.id
method = "GET"
uri = "https://crm.example.com/api/customers/@{triggerBody()?['orderId']}"
depends_on = [module.order_trigger]
}
# 5 · A control-flow action — this module, because If has no typed resource.
module "high_value_check" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-logic-app-action-custom.git?ref=v1.0.0"
name = "If_High_Value"
logic_app_id = module.order_workflow.id
body = jsonencode({
type = "If"
expression = {
and = [{ greater = ["@int(triggerBody()?['amount'])", 10000] }]
}
actions = {
Compose_Escalation = {
type = "Compose"
inputs = "Escalating order @{triggerBody()?['orderId']} for @{body('${module.fetch_customer.name}')?['name']}"
}
}
else = { actions = {} }
runAfter = {
(module.fetch_customer.name) = ["Succeeded"]
}
})
# Serialise definition writes; also expresses the real ordering.
depends_on = [module.fetch_customer]
}
# 6 · A catch block — also this module, since it is a Compose with a Failed runAfter.
module "on_failure" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-logic-app-action-custom.git?ref=v1.0.0"
name = "Compose_Failure_Note"
logic_app_id = module.order_workflow.id
body = jsonencode({
type = "Compose"
inputs = "Order @{triggerBody()?['orderId']} failed: @{result('${module.fetch_customer.name}')}"
runAfter = {
(module.fetch_customer.name) = ["Failed", "TimedOut"]
}
})
depends_on = [module.high_value_check]
}💡 This wiring shows the division of labour: resource group → workflow → trigger → a typed action where one exists → this module for the control-flow and catch actions the provider does not model. Two things to carry away — the
depends_onchain serialises writes to the one shared workflow definition, and everyrunAfterkey is wired from an upstream module'snameso a rename cannot silently break the graph. Output names on sibling modules are illustrative; match them to the versions you pin.
Required: name, logic_app_id, body.
Universal tail: timeouts. This resource type does not support tags.
Full object() schemas
variable "name" {
type = string # force-new; must be unique across ALL actions in the workflow (unchecked by the provider)
}
variable "logic_app_id" {
type = string # force-new
}
variable "body" {
type = string # the raw Logic Apps action JSON; NOT validated by Terraform
}
variable "timeouts" {
type = object({ create = optional(string), read = optional(string), update = optional(string), delete = optional(string) })
default = null
}| Output | Description | Kind |
|---|---|---|
id |
Terraform's composed ID for the action: the workflow's Resource ID with "/actions/" appended | Passthrough |
name |
The action name, which is also its key under definition.actions and the string sibling actions must use in their own runAfter maps | Passthrough |
logic_app_id |
Resource ID of the parent Logic App workflow | Passthrough |
workflow_name |
The parent workflow's name, parsed out of logic_app_id | Derived |
resource_group_name |
Resource group holding the parent workflow, parsed out of logic_app_id | Derived |
subscription_id |
Subscription GUID parsed out of logic_app_id | Derived |
action_type |
The action's declared type, matched case-insensitively against a list of types evidenced in Microsoft's Workflow Definition Language reference and in the provider's own test suite, and emitted as the canonical spelling FROM THAT LITERAL LIST | Derived |
action_type_is_recognized |
True when the body's declared type matched the evidenced list above | Derived |
top_level_keys_present |
Which of the documented top-level action elements this body declares, reported as the INTERSECTION of the body's keys with a literal list written in this file -- so the value can only ever contain strings from that list, never a key invented by the body | Derived |
body_byte_length |
Length of the body document in bytes | Passthrough |
declares_run_after |
True when the body carries a runAfter map with at least one entry, meaning this action waits for a named sibling | Derived |
run_after_dependency_count |
How many sibling actions this action waits on, counted from the keys of runAfter | Derived |
runs_immediately_after_the_trigger |
True when runAfter is absent or empty, so this action starts as soon as the workflow triggers | Derived |
run_after_statuses_used |
Which predecessor statuses this action's runAfter map reacts to, reported as the intersection of the statuses found in the body with a literal list written in this file | Derived |
credential_indicators_present |
Map of literal substrings that commonly accompany an embedded credential, to whether each appears anywhere in the body, matched case-insensitively | Derived |
declares_inline_authentication |
True when the string "authentication" appears anywhere in the body, including inside a nested action of a Scope, If or Foreach | Passthrough |
uses_managed_identity_authentication |
True when the body names ManagedServiceIdentity as an authentication type | Passthrough |
body_is_stored_in_state_in_plaintext |
Constant true | Constant |
reading_the_workflow_reveals_every_action_body |
Constant true | Constant |
action_is_not_an_azure_resource |
Constant true | Constant |
every_write_rewrites_the_entire_parent_workflow |
Constant true | Constant |
destroy_removes_one_key_and_rewrites_the_workflow |
Constant true | Constant |
destroy_sends_the_workflow_without_its_identity_block |
Constant true, and the sharpest edge in this module | Constant |
workflow_enabled_state_is_omitted_from_every_write |
Constant true | Constant |
concurrent_writes_are_serialized_only_inside_one_terraform_process |
Constant true, and the failure it describes is silent | Constant |
body_updates_in_place |
Constant true | Constant |
changing_name_or_logic_app_id_replaces_the_action |
Constant true | Constant |
action_names_are_not_checked_for_uniqueness |
Constant true | Constant |
run_after_targets_are_resolved_only_by_azure_at_apply |
Constant true | Constant |
body_diffs_are_suppressed_only_for_key_order_and_whitespace |
Constant true | Constant |
this_module_ships_no_tags_variable |
Constant true | Constant |
No secret is accepted or emitted. The action body is configuration; secrets it needs belong in a connection or a workflow parameter.
🔴 Destroying an action rewrites the parent workflow and DROPS its managed identity. There is no delete API for a custom action, so the provider reads the whole workflow, removes the action, and PUTs it back. The update path carries
Identity: read.Model.Identity; the remove path builds the workflow without any Identity field. Because this is a full PUT, the workflow loses its managed identity -- and every role assignment granted to that identity -- as a side effect of deleting one action. Neither path sends the workflow'sStateeither. Verify the parent workflow's identity after any destroy that touches an action.
- This module trades type safety for reach, deliberately. Terraform checks that
bodyis a string and nothing more. A wrongtype, a malformed expression, or a reference to a connection that does not exist all surface at apply or at run time. That is acceptable for an action the provider does not model — and needless for an HTTP call, whereterraform-azurerm-logic-app-action-httpgives a validated method enum and an HTTPS-enforced URI. - No validation is offered on purpose. A JSON-shape check would pass any well-formed document, including one that is entirely wrong for Logic Apps, and would imply an assurance the module cannot provide. Documenting what is unchecked is more honest than a check that catches nothing real.
- Every action resource rewrites the same workflow definition. Actions and triggers are not independent child resources in the API — they are parts of one document. the provider DOES serialise them -- it takes a per-workflow mutex. That mutex is in-process only, so it protects components within a single apply and nothing else: two concurrent applies, or an apply overlapping a portal edit, silently lose one side's changes, because every write is a read-modify-write of the whole workflow, so a workflow with many action modules can fail with update conflicts. A
depends_onchain fixes it, and ordering byrun_afterdependencies usually produces that chain anyway. - This mechanism and an inline definition are mutually exclusive. If
terraform-azurerm-logic-app-workflowalso carries a definition, the two fight on every apply. Pick one owner per workflow. nameis an identifier, not a label. Other actions reference it in their ordering, and it is part of the Terraform-composedid. Renaming replaces the action and orphans anything pointing at the old name, so the module emitsnamefor wiring.- Nested actions are not separate resources. Actions inside a
Foreach,Scope,If, orUntillive in that action's own body. Creating them again as their own modules duplicates them. - Secrets do not belong in the body. It is stored in the workflow definition and readable by anyone with read access to the Logic App. Use a workflow parameter sourced from Key Vault, or an API connection.
features {}dependence. The module carries noprovider {}block. If it appears not to initialize in isolation, the cause is a missing caller-sideprovider "azurerm" { features {} }.
| Concern | Secure default (empty call) | Opt-out (caller must type it) |
|---|---|---|
| Typed alternative | documentation steers to logic-app-action-http for HTTP calls |
use this module for an HTTP action anyway |
| Secrets in the definition | steered to a workflow parameter or connection | inline a credential in body |
| Body correctness | documented as unchecked, rather than implied safe | — (no validation is possible) |
| Definition ownership | documented as one mechanism per workflow | mix with an inline workflow definition |
| Reviewability | file() / jsonencode() steered over heredocs |
inline heredoc |
terraform init -backend=false
terraform validate
terraform fmt -check- Pin the module with
?ref=v1.0.0— never a branch. - This library is plan-only during authoring; a human runs
terraform plan/applyfrom CI against real credentials. - Because
bodyis unchecked, a clean plan says little about whether the action works. Exercise the workflow after apply.
terraform validateproves the configuration is internally consistent and type-correct against the pinned provider schema. For this module that is a narrow claim: it confirms three strings are present, not that the action is valid.terraform fmt -checkenforces canonical formatting.- Neither command calls Azure. Only
terraform plan(run by a human, from CI) exercises the ARM API — the module ships without any cloud apply. Whether the body is a valid action, whether a referenced connection exists and is authorized, and whether arunAfternames a real action are all apply-time or run-time facts.
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
id = "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-integration-eastus2/providers/Microsoft.Logic/workflows/wf-orders/actions/If_High_Value"
name = "If_High_Value"
logic_app_id = "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-integration-eastus2/providers/Microsoft.Logic/workflows/wf-orders"
| Symptom | Cause | Fix |
|---|---|---|
Provider configuration not present / features error |
No caller-side provider "azurerm" { features {} }. |
Add the provider block with features {} in the root module. |
| Apply fails with a workflow-update conflict | Several action or trigger resources wrote the same workflow definition concurrently. | Chain them with depends_on, ideally in run_after order. |
| Actions disappear after an apply | The workflow module also carries an inline definition, which overwrote them. | Pick one mechanism per workflow — inline definition or separate action resources. |
| Apply succeeds, the workflow fails at run time | body is not validated by Terraform. |
Check the run history for the actual error; a wrong type or expression is the usual cause. |
| Run fails: connection not found or unauthorized | The API connection does not exist, or its interactive consent was never completed. | Create and authorize the connection first; Terraform cannot complete a connector's consent flow. |
A runAfter reference does nothing |
It names an action that does not exist, or the name was renamed. | Wire the key from the upstream module's name output instead of typing it. |
| Duplicate actions in the workflow | Nested actions were also created as their own module instances. | Nested actions belong in the parent action's body only. |
| Plan wants to replace the action after a rename | name is force-new. |
Expected; update anything referencing the old name in the same apply. |
terraform import cannot find the action |
The ID is Terraform-composed and no Azure API returns it. | Compose it: workflow Resource ID + /actions/<name>. |
- azurerm provider —
azurerm_logic_app_action_custom - Logic Apps workflow definition language — actions
- Control-flow actions: conditions, switches, and loops
- Sibling modules:
terraform-azurerm-logic-app-workflow,terraform-azurerm-logic-app-action-http,terraform-azurerm-logic-app-trigger-custom,terraform-azurerm-logic-app-trigger-http-request,terraform-azurerm-logic-app-trigger-recurrence,terraform-azurerm-logic-app-integration-account. - This module's
SCOPE.md.
💙 "Infrastructure as Code should be standardized, consistent, and secure."