How to Use for_each to Create Multiple Queues from a YAML Variable File in Terraform

How to Use for_each to Create Multiple Queues from a YAML Variable File in Terraform

What You Will Build

  • You will provision a dynamic set of Genesys Cloud CX queues by iterating over a list of definitions stored in a YAML file.
  • This tutorial uses the for_each meta-argument in Terraform to map YAML entries to individual genesyscloud_routing_queue resources.
  • The implementation is written in HCL (HashiCorp Configuration Language) using the official Genesys Cloud Terraform Provider.

Prerequisites

  • Terraform Version: 1.5.0 or later.
  • Genesys Cloud Terraform Provider: Version 1.10.0 or later.
  • Genesys Cloud Organization: An active Genesys Cloud CX organization with an API user that has routing:queue:write and routing:queue:view scopes.
  • Dependencies:
    • yaml parser support is native in Terraform via the yamldecode function.
    • No external Python or Node.js dependencies are required.

Authentication Setup

Terraform handles OAuth authentication for Genesys Cloud automatically when the provider is configured. You do not need to write manual token exchange code. Instead, you configure the provider block with your credentials or environment variables.

Provider Configuration

Create a file named main.tf. This file initializes the provider and defines the data source that reads the YAML file.

terraform {
  required_providers {
    genesyscloud = {
      source  = "myntra/genesyscloud"
      version = "~> 1.10.0"
    }
  }
}

provider "genesyscloud" {
  # Credentials can be passed via environment variables:
  # GENESYS_CLOUD_REGION, GENESYS_CLOUD_CLIENT_ID, GENESYS_CLOUD_CLIENT_SECRET
  # Or explicitly here (not recommended for production):
  # region = "us-east-1"
  # client_id = "your_client_id"
  # client_secret = "your_client_secret"
}

The YAML Variable File

Create a file named queues.yaml. This file contains the list of queues to be created. Each entry must include a unique name and a unique external_id (used by Terraform to track state).

queues:
  - name: "Sales Support - Tier 1"
    external_id: "sales-t1"
    description: "First line of support for sales inquiries"
    split_workflow_id: null
    enabled: true
    queue_flow: "longest_available_agent"
    default_outbound_email: ""
    wrap_up_policy: "optional"
  - name: "Sales Support - Tier 2"
    external_id: "sales-t2"
    description: "Escalated issues for sales team"
    split_workflow_id: null
    enabled: true
    queue_flow: "shortest_queue"
    default_outbound_email: ""
    wrap_up_policy: "required"
  - name: "Technical Support - Billing"
    external_id: "tech-billing"
    description: "Billing and payment issues"
    split_workflow_id: null
    enabled: true
    queue_flow: "longest_available_agent"
    default_outbound_email: ""
    wrap_up_policy: "optional"

Implementation

Step 1: Parse the YAML File into a Terraform Map

Terraform does not iterate directly over YAML lists using for_each. The for_each argument requires a map or a set of strings. Therefore, you must transform the YAML list into a map where the keys are stable identifiers (such as external_id) and the values are the queue configurations.

Use the yamldecode function to parse the file and the for expression to convert the list into a map.

Add this to main.tf:

# Read the YAML file
locals {
  # yamldecode returns a map. We access the 'queues' key which contains the list.
  raw_queues = yamldecode(file("${path.module}/queues.yaml"))["queues"]

  # Convert the list of maps into a map keyed by external_id.
  # This allows for_each to handle creation, update, and deletion of specific queues.
  queue_map = {
    for q in local.raw_queues : q.external_id => q
  }
}

Why this matters: If you use count instead of for_each, adding a new queue to the middle of the YAML list would force Terraform to destroy and recreate all subsequent queues because their indices would shift. for_each uses the external_id as a stable key, so adding or removing an entry only affects that specific resource.

Step 2: Define the Queue Resource with for_each

Now, define the genesyscloud_routing_queue resource. This resource maps directly to the Genesys Cloud API endpoint POST /api/v2/routing/queues.

The for_each meta-argument iterates over local.queue_map. Inside the block, each.value refers to the specific queue object from the YAML file.

Add this to main.tf:

resource "genesyscloud_routing_queue" "dynamic_queues" {
  for_each = local.queue_map

  name         = each.value.name
  description  = each.value.description
  enabled      = each.value.enabled
  external_id  = each.value.external_id

  # Queue Flow: Determines how Genesys routes conversations to agents.
  # Valid values: "longest_available_agent", "shortest_queue", "random", "balanced"
  queue_flow   = each.value.queue_flow

  # Wrap-up Policy: Determines if agents must mark conversations as complete.
  # Valid values: "optional", "required"
  wrap_up_policy = each.value.wrap_up_policy

  # Default Outbound Email: Used if the queue sends emails.
  # If not set, leave as empty string or omit.
  default_outbound_email = each.value.default_outbound_email != "" ? each.value.default_outbound_email : null

  # Split Workflow ID: Optional. Links the queue to a specific IVR/Flow.
  # If null in YAML, Terraform will not set this field.
  dynamic "split_workflow" {
    for_each = each.value.split_workflow_id != null ? [each.value.split_workflow_id] : []
    content {
      id = split_workflow.value
    }
  }

  # Language Settings: Genesys requires at least one language setting.
  # We default to English if not specified in YAML.
  languages {
    language_code = "en-US"
    is_default    = true
  }

  # Skill Requirements: Optional. Add if your queues require specific skills.
  # This example leaves it empty for simplicity, but in production, you might
  # parse a list of skill IDs from the YAML.
  # skill_requirements {
  #   skill_id = "some-skill-id"
  #   level    = 500
  #   required = true
  # }
}

API Mapping Note:

  • name maps to name in the API body.
  • external_id maps to external_id. This is critical for idempotency.
  • queue_flow maps to queue_flow.
  • wrap_up_policy maps to wrap_up_policy.
  • languages maps to language_settings. The provider expects a list of objects.

Step 3: Handle Dependencies and Outputs

If other resources (such as genesyscloud_routing_user or genesyscloud_ivr) need to reference these queues, you must reference them by their external_id key in the genesyscloud_routing_queue.dynamic_queues map.

Add this to main.tf to output the created queue IDs for verification:

output "created_queue_ids" {
  description = "Map of external_id to Genesys Cloud Queue ID"
  value       = { for k, v in genesyscloud_routing_queue.dynamic_queues : k => v.id }
}

Complete Working Example

Below is the complete main.tf file. Create this file in a new directory, create the queues.yaml file as shown above, and run terraform init followed by terraform apply.

terraform {
  required_providers {
    genesyscloud = {
      source  = "myntra/genesyscloud"
      version = "~> 1.10.0"
    }
  }
}

provider "genesyscloud" {
  # Ensure GENESYS_CLOUD_CLIENT_ID and GENESYS_CLOUD_CLIENT_SECRET are set in your environment
}

locals {
  # Parse the YAML file
  raw_queues = yamldecode(file("${path.module}/queues.yaml"))["queues"]

  # Convert list to map keyed by external_id for stable for_each iteration
  queue_map = {
    for q in local.raw_queues : q.external_id => q
  }
}

resource "genesyscloud_routing_queue" "dynamic_queues" {
  for_each = local.queue_map

  name         = each.value.name
  description  = each.value.description
  enabled      = each.value.enabled
  external_id  = each.value.external_id

  queue_flow   = each.value.queue_flow
  wrap_up_policy = each.value.wrap_up_policy
  
  # Handle optional default_outbound_email
  default_outbound_email = each.value.default_outbound_email != "" ? each.value.default_outbound_email : null

  # Handle optional split_workflow_id
  dynamic "split_workflow" {
    for_each = each.value.split_workflow_id != null ? [each.value.split_workflow_id] : []
    content {
      id = split_workflow.value
    }
  }

  languages {
    language_code = "en-US"
    is_default    = true
  }
}

output "created_queue_ids" {
  description = "Map of external_id to Genesys Cloud Queue ID"
  value       = { for k, v in genesyscloud_routing_queue.dynamic_queues : k => v.id }
}

Execution Steps

  1. Initialize:
    terraform init
    
  2. Plan:
    terraform plan
    
    Verify that Terraform shows +3 to add (or however many queues are in your YAML).
  3. Apply:
    terraform apply
    
    Type yes to confirm.

Common Errors & Debugging

Error: Error: Invalid index

Cause: The yamldecode function fails because the YAML file is malformed or the key path is incorrect. For example, if your YAML file does not have a top-level queues: key, yamldecode(...)[...] will return null.

Fix: Validate your YAML syntax. Ensure the top-level key matches the one you are accessing in locals.

# Debugging: Output the raw parsed data to see its structure
output "debug_raw" {
  value = local.raw_queues
}

Error: Error: Queue with external_id 'sales-t1' already exists

Cause: You are trying to create a queue with an external_id that already exists in Genesys Cloud but is not managed by Terraform.

Fix: Either delete the existing queue in Genesys Cloud and re-run terraform apply, or use terraform import to bring the existing queue under Terraform management.

terraform import genesyscloud_routing_queue.dynamic_queues["sales-t1"] <genesys_cloud_queue_id>

Error: Error: Attribute "queue_flow" is required

Cause: The YAML file does not include a queue_flow field, or it is set to null. The Genesys Cloud API requires a valid queue flow type.

Fix: Ensure every entry in queues.yaml has a valid queue_flow value. Valid values are:

  • longest_available_agent
  • shortest_queue
  • random
  • balanced

Error: 429 Too Many Requests

Cause: Creating many queues in a single terraform apply can trigger Genesys Cloud API rate limits.

Fix: The Genesys Cloud Terraform Provider includes built-in retry logic for 429 errors. However, if you are creating hundreds of queues, you may need to split the YAML file into smaller chunks or add a time_sleep resource between batches (though this is rarely necessary for small-to-medium deployments).

Official References