Automate Genesys Cloud Queue Provisioning with Terraform and YAML Variables
What You Will Build
- A Terraform configuration that dynamically creates multiple Genesys Cloud CX queues by parsing a YAML variable file.
- An implementation using the
yamldecodefunction andfor_eachmeta-argument to map YAML data to Genesys resources. - A working Python script to generate the YAML input file and a Terraform module to consume it.
Prerequisites
- Terraform Version: 1.5+ (required for robust
yamldecodeandfor_eachstability). - Genesys Cloud Provider:
genesyscloud/genesyscloudversion 1.15.0+. - Runtime: Python 3.9+ with
pyyamlinstalled (pip install pyyaml). - Authentication: Genesys Cloud OAuth Client credentials (Client ID and Client Secret) with the following scopes:
queue:write(to create/update queues)routing:write(if configuring routing profiles or skills)user:read(to assign users to queues, if applicable)
Authentication Setup
Terraform handles OAuth token management automatically when you provide the client credentials. You must not manage tokens manually in the Terraform code. Instead, you configure the provider block to use environment variables or a credentials file.
For this tutorial, we assume the credentials are stored in environment variables: GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET.
terraform {
required_providers {
genesyscloud = {
source = "genesyscloud/genesyscloud"
version = "~> 1.15.0"
}
}
}
provider "genesyscloud" {
# Terraform reads these from environment variables automatically
# GENESYS_CLIENT_ID=your_client_id
# GENESYS_CLIENT_SECRET=your_client_secret
# Optional: Explicitly set base URL if using a non-standard region
# base_url = "https://api.mypurecloud.com"
}
If you receive a 401 Unauthorized error, verify that the client ID and secret are correct and that the client has the queue:write scope assigned in the Genesys Cloud Admin Center under Developers > OAuth Clients.
Implementation
Step 1: Generate the YAML Variable File
Before writing Terraform, you need a structured data source. YAML is preferred over JSON for manual editing because it is more readable. We will use a Python script to generate a realistic queues.yaml file. This simulates a scenario where queue definitions are managed externally (e.g., by a DevOps pipeline or a configuration management tool).
Create a file named generate_queues.py:
import yaml
import sys
def generate_queue_config(filename: str = "queues.yaml") -> None:
"""
Generates a YAML configuration file for multiple Genesys Cloud Queues.
Each queue entry contains all necessary fields for the terraform_genesis_cloud_queue resource.
"""
queues_data = {
"queues": [
{
"name": "Sales - North America",
"description": "Handles inbound sales calls for the North American region.",
"outbound_enabled": False,
"wrap_up_policy": "optional",
"type": "internal",
"skills": ["sales", "english"],
"member_flow": "longest_idle",
"utilization_basis": "occupancy",
"queue_level_objects": {
"enabled": True,
"member_flow": "longest_idle"
},
"skill_filter_policy": "or"
},
{
"name": "Support - Technical Tier 2",
"description": "Escalated technical support issues requiring deep expertise.",
"outbound_enabled": True,
"wrap_up_policy": "required",
"type": "internal",
"skills": ["technical_support", "tier2", "english"],
"member_flow": "agent_skill",
"utilization_basis": "occupancy",
"queue_level_objects": {
"enabled": True,
"member_flow": "agent_skill"
},
"skill_filter_policy": "and"
},
{
"name": "Billing - Inquiries",
"description": "Customer billing questions and payment disputes.",
"outbound_enabled": False,
"wrap_up_policy": "optional",
"type": "internal",
"skills": ["billing", "english"],
"member_flow": "longest_idle",
"utilization_basis": "occupancy",
"queue_level_objects": {
"enabled": True,
"member_flow": "longest_idle"
},
"skill_filter_policy": "or"
}
]
}
with open(filename, 'w') as f:
yaml.dump(queues_data, f, default_flow_style=False, sort_keys=False)
print(f"Generated {filename} successfully.")
if __name__ == "__main__":
generate_queue_config()
Run the script:
python generate_queues.py
This produces a queues.yaml file. The structure is critical: the top-level key queues holds a list of dictionaries. Each dictionary maps directly to the arguments of the genesys_cloud_routing_queue Terraform resource.
Step 2: Define the Terraform Variable and Data Source
In your main.tf, you must read the YAML file and parse it into a Terraform map. Terraform does not natively support iterating over a list of maps with for_each unless you convert the list into a map where the key is unique.
We will use the yamldecode function to parse the file and a local value to transform the list into a map keyed by the queue name. Queue names must be unique in Genesys Cloud, so using the name as the key is safe for this specific use case.
# main.tf
# Read the YAML file content
locals {
# Load the YAML file
raw_yaml_content = file("${path.module}/queues.yaml")
# Parse YAML into a Terraform map
parsed_yaml = yamldecode(local.raw_yaml_content)
# Extract the list of queues
queue_list = local.parsed_yaml.queues
# Transform the list into a map for use with for_each.
# The key is the queue name, and the value is the entire queue object.
# This allows us to iterate over unique identifiers.
queue_map = { for q in local.queue_list : q.name => q }
}
# Define the variable to allow overriding the file path if needed
variable "queue_config_file" {
description = "Path to the YAML file containing queue definitions"
type = string
default = "${path.module}/queues.yaml"
}
Why this transformation is necessary:
The for_each meta-argument in Terraform requires a map or a set of strings. It does not accept a list of objects directly because Terraform cannot guarantee the order of elements in a list during plan/apply cycles, which leads to unnecessary resource recreation. By converting the list to a map keyed by name, we provide a stable identifier for each resource.
Step 3: Create the Queues with for_each
Now, define the genesys_cloud_routing_queue resource using for_each. You will iterate over local.queue_map.
resource "genesys_cloud_routing_queue" "dynamic_queues" {
for_each = local.queue_map
name = each.value.name
description = each.value.description
type = each.value.type
# Boolean flags
outbound_enabled = each.value.outbound_enabled
wrap_up_policy = each.value.wrap_up_policy
# Routing logic
member_flow = each.value.member_flow
utilization_basis = each.value.utilization_basis
# Skill configuration
# The Genesys Cloud Terraform provider expects a list of strings for skills
skills = each.value.skills
# Skill filter policy determines how skills are matched (AND vs OR)
skill_filter_policy = each.value.skill_filter_policy
# Queue Level Objects (QLO)
# This block configures specific routing behaviors for the queue
dynamic "queue_level_objects" {
for_each = each.value.queue_level_objects.enabled ? [1] : []
content {
enabled = queue_level_objects.value.enabled
member_flow = queue_level_objects.value.member_flow
}
}
# Lifecycle rule to prevent Terraform from removing skills not defined in YAML
# if they were added manually in the UI. This is optional but recommended
# for mixed-managed environments.
lifecycle {
ignore_changes = [
# Uncomment if you want to ignore manual changes to skills
# skills
]
}
}
Key Implementation Details:
each.value: Inside the resource block,eachrefers to the current item in the iteration.each.valueis the queue object from the YAML map, andeach.keyis the queue name.dynamicBlock forqueue_level_objects: Thequeue_level_objectsblock in the Genesys Cloud provider is optional. In our YAML, we used a boolean flagenabledto control whether this block should be present. Thedynamicblock allows us to conditionally include this nested block only ifenabledis true. Ifenabledis false, the block is omitted entirely, preventing configuration drift errors.- Skills as a List: The
skillsfield in the provider expects a list of strings. Our YAML structure provides this directly. If your YAML used a different structure (e.g., an object with skill IDs), you would need to map it using aforexpression inside the resource.
Step 4: Handling Edge Cases and Dependencies
If your queues depend on specific skills existing in Genesys Cloud, you must ensure those skills are created first. Terraform manages dependencies automatically if you reference the skill resources. However, if skills are managed outside of Terraform (e.g., manually or by another pipeline), you must ensure they exist before applying the queue configuration.
If you are also creating skills from a YAML file, you would do something similar:
# Example: Creating skills from a separate YAML list
locals {
skills_list = ["sales", "english", "technical_support", "tier2", "billing"]
}
resource "genesys_cloud_routing_skill" "dynamic_skills" {
for_each = toset(local.skills_list)
name = each.value
type = "queue"
}
# Then, in the queue resource, you must reference the skill IDs if you want strict dependency management.
# However, the genesys_cloud_routing_queue resource accepts skill NAMES, not IDs, in recent provider versions.
# Therefore, explicit dependency management via 'depends_on' is often sufficient.
resource "genesys_cloud_routing_queue" "dynamic_queues" {
for_each = local.queue_map
# ... other attributes ...
skills = each.value.skills
# Ensure skills are created before queues
depends_on = [genesys_cloud_routing_skill.dynamic_skills]
}
Complete Working Example
Below is the complete main.tf file that combines all steps. Save this as main.tf in a directory alongside queues.yaml.
terraform {
required_providers {
genesyscloud = {
source = "genesyscloud/genesyscloud"
version = "~> 1.15.0"
}
}
}
provider "genesyscloud" {
# Credentials are expected in environment variables:
# GENESYS_CLIENT_ID
# GENESYS_CLIENT_SECRET
}
locals {
# Load and parse the YAML file
raw_yaml_content = file("${path.module}/queues.yaml")
parsed_yaml = yamldecode(local.raw_yaml_content)
queue_list = local.parsed_yaml.queues
# Convert list to map keyed by name for for_each
queue_map = { for q in local.queue_list : q.name => q }
}
# Create Skills (Optional but recommended for full automation)
resource "genesys_cloud_routing_skill" "required_skills" {
for_each = toset(flatten([for q in local.queue_list : q.skills]))
name = each.value
type = "queue"
}
# Create Queues
resource "genesys_cloud_routing_queue" "dynamic_queues" {
for_each = local.queue_map
name = each.value.name
description = each.value.description
type = each.value.type
outbound_enabled = each.value.outbound_enabled
wrap_up_policy = each.value.wrap_up_policy
member_flow = each.value.member_flow
utilization_basis = each.value.utilization_basis
# Skills: Pass the list of skill names directly
skills = each.value.skills
# Skill Filter Policy
skill_filter_policy = each.value.skill_filter_policy
# Conditional Queue Level Objects
dynamic "queue_level_objects" {
for_each = each.value.queue_level_objects.enabled ? [1] : []
content {
enabled = queue_level_objects.value.enabled
member_flow = queue_level_objects.value.member_flow
}
}
# Dependency on skills to ensure they exist before queue creation
depends_on = [genesys_cloud_routing_skill.required_skills]
}
# Output the created queue IDs for verification
output "created_queue_ids" {
description = "Map of queue names to their Genesys Cloud IDs"
value = { for name, queue in genesys_cloud_routing_queue.dynamic_queues : name => queue.id }
}
To run this configuration:
- Set your environment variables:
export GENESYS_CLIENT_ID="your_client_id" export GENESYS_CLIENT_SECRET="your_client_secret" - Initialize Terraform:
terraform init - Plan the changes:
terraform plan - Apply the configuration:
terraform apply
Common Errors & Debugging
Error: Error: Invalid index or Unsupported attribute
Cause: The YAML file structure does not match the expected keys in the Terraform code. For example, if you misspell member_flow in the YAML as memberflow, Terraform will fail when trying to access each.value.memberflow.
Fix: Validate your YAML structure against the Terraform resource arguments. Use the yamldecode function in the Terraform console to inspect the parsed data:
terraform console
> yamldecode(file("queues.yaml"))
Ensure every key accessed in the resource block exists in the parsed output.
Error: Error: Conflicting configuration arguments
Cause: You are defining a block both statically and dynamically, or you have conflicting values for the same attribute. This often happens with queue_level_objects if you define it manually and also use a dynamic block.
Fix: Ensure you only use one method to define a block. If using dynamic, do not also define the block manually in the resource.
Error: Error: 400 Bad Request: The queue name '...' already exists
Cause: You are trying to create a queue with a name that already exists in your Genesys Cloud organization. Queue names must be unique.
Fix: Check your Genesys Cloud Admin Center for existing queues with the same name. Rename the queue in your queues.yaml file or delete the existing queue in Genesys Cloud if it is no longer needed.
Error: Error: Provider produced inconsistent result after apply
Cause: The Genesys Cloud API returns a different value than what was sent. This often happens with boolean flags or nested objects where the API has default values that differ from the Terraform configuration.
Fix: Review the lifecycle block in the resource. If the API returns a default value for an optional field that you did not specify, Terraform may perceive this as a drift. You can ignore specific attributes using ignore_changes in the lifecycle block if they are not critical to your configuration.