What are Bicep Decorators

Make your Azure Bicep templates smarter, safer, and self-documenting using decorators

When you start building Azure resources with Bicep, you’ll quickly run into these strange little @ symbols floating above your parameters:

1
2
3
@description('The environment name for the deployment.')
@allowed(['dev', 'test', 'prod'])
param environment string

They look harmless enough, but what’s actually happening here?

What Are Bicep Decorators?

Bicep decorators — one of the most underrated features in Infrastructure-as-Code, are like annotations — bits of metadata or behavior you can attach to parameters, variables, resources, and outputs.

They don’t change what gets deployed. They change how it’s validated, displayed, or controlled during deployment.

If you’ve ever used attributes in C# or decorators in Python/TypeScript, the concept will feel familiar.

Why Decorators Matter

Most Bicep templates start life simple — a few parameters, a couple of resources, and some outputs. Then, as environments scale, those templates become shared, reused, and parameterized by multiple teams.

That’s where decorators shine:

  • âś… They make templates self-documenting
  • đź§  They add input validation
  • đź§± They integrate with portal UI metadata
  • 🕵️ They make your code easier to read and maintain

In other words: they turn your IaC from “it works” to “it scales.”

Common Decorators

Here’s a quick cheat sheet of the decorators you’ll actually use day to day:

DecoratorDescriptionExample
@allowed([...])Restricts parameter values to a set of options.@allowed(['dev','test','prod'])
@batch(1)Control loop concurrency during deployment.@batch(1)
@description('…')Adds inline documentation for tooling and readers.@description('Admin username for the VM.')
@discriminator('…')Used for tagged union types (advanced).—
@export()Expose elements for import in other files.—
@metadata({…})Attach structured data used by tooling or AVM.@metadata({ category: 'network' })
@minLength(n) / @maxLength(n)Enforces input length limits.@minLength(3)
@minValue(n) / @maxValue(n)Numeric limits for integer parameters.@maxValue(5)
@sealed()Prevents typos in object properties.@sealed() param tags object
@secure()Hides secrets from deployment logs.@secure() param password string

Example: Clean, Validated Parameters

Here’s what a production-ready parameter block should look like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@description('The Azure region where resources will be deployed.')
@allowed([
  'uksouth'
  'ukwest'
  'westeurope'
  'northeurope'
])
param location string = 'westeurope'

@secure()
@description('Administrator password for the virtual machine.')
param adminPassword string

Result: Anyone using this module instantly knows what each value is, what’s valid, and what’s sensitive — no extra documentation needed.


The Hidden Power of @batch(1)

Most decorators handle metadata or validation. But there’s one that actually changes deployment behavior — @batch().

When you create multiple resources in a loop, Bicep deploys them in parallel by default. That’s fast, but not always safe.

To start this first we need to create some very basic IaC, a resource group and a virtual network

Initial IaC - Virtual Network
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
targetScope = 'subscription'

param customerName string = 'bwc'

@description('Deployment Location')
param location string = 'westeurope'

@description('Location Short Code')
param locationShortCode string = 'weu'

@description('Resource Group Name')
param resourceGroupName string = 'rg-x-${customerName}-bicep'

@description('Virtual Network Name')
param virtualNetworkName string = 'vnet-${customerName}-bicep-${locationShortCode}'

@description('Virtual Network Address Prefix')
param virtualNetworkAddressPrefix string = '10.0.0.0/16'

//
// Azure Modules
//

module createResourceGroup 'br/public:avm/res/resources/resource-group:0.4.2' = {
  name: 'create-resource-group'
  params:{
    name: resourceGroupName
    location: location
  }
}

module createVirtualNetwork 'br/public:avm/res/network/virtual-network:0.7.1' = {
  name: 'create-virtual-network'
  scope: resourceGroup(resourceGroupName)
  params:{
    name: virtualNetworkName
    location: location
    addressPrefixes: [
      virtualNetworkAddressPrefix
    ]
  }
}

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
targetScope = 'subscription'

param customerName string = 'bwc'

@description('Deployment Location')
param location string = 'westeurope'

@description('Location Short Code')
param locationShortCode string = 'weu'

@description('Resource Group Name')
param resourceGroupName string = 'rg-x-${customerName}-bicep'

@description('Virtual Network Name')
param virtualNetworkName string = 'vnet-${customerName}-bicep-${locationShortCode}'

@description('Array of Subnets to create')
var subnetArray = [
  {
    name: 'subnet-1'
    addressPrefix: '10.0.1.0/24'
  }
  {
    name: 'subnet-2'
    addressPrefix: '10.0.2.0/24'
  }
    {
    name: 'subnet-3'
    addressPrefix: '10.0.3.0/24'
  }
]

//
// Azure Modules
//

resource existingVirtualNetwork 'Microsoft.Network/virtualNetworks@2024-10-01' existing = {
  name: virtualNetworkName
  scope: resourceGroup(resourceGroupName)
}


module createSubnets 'br/public:avm/res/network/virtual-network/subnet:0.1.3' = [for subnet in subnetArray: {
  name: 'createSubnet-${subnet.name}'
  scope: resourceGroup(resourceGroupName)
  params: {
    virtualNetworkName: existingVirtualNetwork.name
    name: subnet.name
    addressPrefix: subnet.addressPrefix
  }
  dependsOn: [
    existingVirtualNetwork
  ]
}]

If you’ve ever hit this error 👇

Each subnet creation updates the parent VNet’s metadata — and Azure only allows one update at a time. When ARM tries to deploy them all together, it collides with itself.

The Solution

Add the batchSize() decorator to the createSubnets module:

1
@batchSize(1)

The complete module, including the decorator:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@batchSize(1)
module createSubnets 'br/public:avm/res/network/virtual-network/subnet:0.1.3' = [for subnet in subnetArray: {
  name: 'createSubnet-${subnet.name}'
  scope: resourceGroup(resourceGroupName)
  params: {
    virtualNetworkName: existingVirtualNetwork.name
    name: subnet.name
    addressPrefix: subnet.addressPrefix
  }
  dependsOn: [
    existingVirtualNetwork
  ]
}]

Now subnets deploy sequentially, one at a time — avoiding the metadata lock.
You can even tune it (e.g., @batch(3)), but for subnets, @batch(1) is the safe bet.

Pro Tip:

Use batching any time you’re looping over resources that share a parent — VNets, App Service Plans, or Key Vaults.


Full List of Supported Decorators (2025)

DecoratorPurpose
@allowed([...])Restrict parameter values
@batch(n)Control parallel loop deployment
@description('…')Add human-readable info
@discriminator('…')Tagged union type handling
@export()Allow cross-file imports
@metadata({…})Attach metadata (for tools)
@minLength(n) / @maxLength(n)String/array validation
@minValue(n) / @maxValue(n)Numeric validation
@sealed()Prevent typos in object keys
@secure()Mark as sensitive

Bicep continues to evolve, so it’s always worth checking the official documentation for the latest supported decorators.


Wrapping Up

Decorators might not grab the headlines, but they make a huge difference in how clean, reliable, and maintainable your Bicep templates become. They turn quick scripts into professional-grade deployments — the kind you can hand off to a team and still sleep at night.

So next time you’re building parameters, outputs, or resource loops, don’t skip the @. Those small details turn good infrastructure into great automation — and your future self will thank you for it.

Share with your network!

Last updated on Oct 21, 2025 00:00 UTC
Built with Hugo - Theme Stack designed by Jimmy