Migrating to Genesys Cloud Terraform Provider v1.35.0: Handling the genesyscloud_user Schema Break
What You Will Build
- One sentence: You will refactor existing Terraform configurations to accommodate the breaking change in the
genesyscloud_userresource schema introduced in provider version 1.35.0, specifically regarding attribute handling for user profiles and routing. - One sentence: This tutorial uses the Genesys Cloud Terraform Provider (HCL) and the Genesys Cloud REST API for validation.
- One sentence: The primary language is HashiCorp Configuration Language (HCL), with Python used for API validation scripts.
Prerequisites
- OAuth Client: A Genesys Cloud OAuth Client with
user:read,user:write, androuting:agentscopes. - Provider Version: Genesys Cloud Terraform Provider
v1.35.0or later. - Runtime: Terraform CLI
v1.5.0or later. - External Dependencies:
- Python
requestslibrary for API validation. - A valid Genesys Cloud organization with at least one active user and one routing queue.
- Python
Authentication Setup
Before executing any Terraform plan or apply, you must authenticate the provider. The Genesys Cloud provider supports multiple authentication methods, but for scripting and CI/CD pipelines, the OAuth Client Credentials flow is the standard.
In your Terraform configuration, you do not write the OAuth flow manually. Instead, you configure the provider block to source credentials from environment variables. This keeps secrets out of your codebase.
terraform {
required_providers {
genesyscloud = {
source = "mycon/genesyscloud"
version = ">= 1.35.0"
}
}
}
provider "genesyscloud" {
# These environment variables must be set before running terraform
# GC_OAUTH_CLIENT_ID
# GC_OAUTH_CLIENT_SECRET
# GC_OAUTH_DOMAIN (e.g., usw2.mygen.com)
}
To validate that your credentials are working before touching the user resource, use this Python script to obtain an access token and verify the user:read scope.
import requests
import os
def get_genesys_token():
"""
Retrieves an OAuth2 token using Client Credentials flow.
"""
client_id = os.getenv("GC_OAUTH_CLIENT_ID")
client_secret = os.getenv("GC_OAUTH_CLIENT_SECRET")
domain = os.getenv("GC_OAUTH_DOMAIN", "mypurecloud.com")
if not client_id or not client_secret:
raise ValueError("GC_OAUTH_CLIENT_ID and GC_OAUTH_CLIENT_SECRET must be set.")
url = f"https://{domain}/oauth/token"
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
data = {
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret
}
response = requests.post(url, headers=headers, data=data)
if response.status_code != 200:
raise Exception(f"Authentication failed: {response.status_code} - {response.text}")
return response.json().get("access_token")
if __name__ == "__main__":
try:
token = get_genesys_token()
print("Authentication successful. Token acquired.")
except Exception as e:
print(f"Error: {e}")
Implementation
Step 1: Identifying the Schema Change
In versions prior to 1.35.0, the genesyscloud_user resource handled certain attributes, specifically those related to routing and user profile extensions, in a way that allowed implicit defaults or looser validation. In v1.35.0, the schema was tightened to align strictly with the Genesys Cloud REST API v2 behavior.
The critical breaking change involves the routing_profile and user_divisions handling, as well as the removal of deprecated fields in favor of explicit user_profile blocks. If you are upgrading from v1.34.x, you will likely encounter a Error: Invalid attribute name or Error: Unsupported argument during terraform plan.
You must first identify which attributes in your existing genesyscloud_user resources are now invalid. The most common offenders are:
- Direct specification of
routing_profile_idwithout a correspondingrouting_profileblock in some complex nested structures. - Use of deprecated
skillsarrays directly on the user resource instead of viarouting_profile. - Missing
division_idwhen creating users in multi-division environments.
Step 2: Refactoring the User Resource
You will now refactor a standard user creation script. In the old version, you might have passed skills directly. In v1.35.0, skills are managed exclusively through the routing_profile. The user resource itself only references the routing_profile by ID.
Here is the correct, compliant HCL structure for genesyscloud_user in v1.35.0.
# 1. Define the Routing Profile first
# Skills are attached here, not on the user
resource "genesyscloud_routing_skill" "skill_general" {
name = "General Support"
description = "General customer support skill"
}
resource "genesyscloud_routing_profile" "profile_support" {
name = "Support Agent Profile"
description = "Profile for general support agents"
# Skills are added to the profile
skills = [genesyscloud_routing_skill.skill_general.id]
# Wrap-up codes are also part of the profile
wrapup_code_groups = []
}
# 2. Define the User Resource
resource "genesyscloud_user" "agent_one" {
name = "Alice Smith"
email = "alice.smith@example.com"
division_id = data.genesyscloud_user_division.default.id
# The user must have a password if created via API
# Note: This is sensitive and should be handled securely
password = "SecurePassword123!"
# Assign the routing profile
routing_profile_id = genesyscloud_routing_profile.profile_support.id
# User extensions (optional but common)
# In v1.35.0, ensure you do not mix legacy extension formats
user_phone_settings {
auto_answer = false
call_waiting = true
}
# Languages (optional)
languages {
language_id = "en-US"
language_order = 1
}
}
Key Changes Explained:
routing_profile_id: This is now a top-level argument on thegenesyscloud_userresource. In previous versions, if you had a complexroutingblock, it may have been nested differently. Ensure you are using the top-levelrouting_profile_id.division_id: You must explicitly reference a division. The provider no longer attempts to guess the default division as reliably. You must fetch the division ID using adatasource.user_phone_settings: This block structure was refined. Ensure you are not using deprecated attributes likedo_not_disturbdirectly here if they have been moved to presence settings in newer API versions.
Step 3: Fetching Dependencies Securely
You cannot hardcode IDs. You must fetch the division_id and routing_profile_id dynamically. This ensures your Terraform state remains valid even if the underlying Genesys Cloud IDs change (though IDs rarely change, names and descriptions do).
# Fetch the default user division
data "genesyscloud_user_division" "default" {
name = "Default"
}
# Fetch an existing routing profile if you are not creating it
data "genesyscloud_routing_profile" "existing_profile" {
name = "Support Agent Profile"
}
resource "genesyscloud_user" "agent_two" {
name = "Bob Jones"
email = "bob.jones@example.com"
division_id = data.genesyscloud_user_division.default.id
password = "SecurePassword456!"
# Reference the fetched profile
routing_profile_id = data.genesyscloud_routing_profile.existing_profile.id
}
Step 4: Validating with the REST API
After applying the Terraform changes, you must verify that the user was created correctly and that the routing profile is attached. You will use the GET /api/v2/users/{userId} endpoint.
import requests
import os
def get_user_details(user_id: str, token: str, domain: str) -> dict:
"""
Retrieves full user details including routing profile ID.
"""
url = f"https://{domain}/api/v2/users/{user_id}"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
response = requests.get(url, headers=headers)
if response.status_code == 401:
raise Exception("Unauthorized: Check your OAuth token.")
if response.status_code == 403:
raise Exception("Forbidden: Check if your client has user:read scope.")
if response.status_code == 404:
raise Exception(f"User not found: {user_id}")
if response.status_code != 200:
raise Exception(f"API Error: {response.status_code} - {response.text}")
return response.json()
def validate_user_routing(user_data: dict) -> bool:
"""
Validates that the user has a routing profile attached.
"""
routing_profile_id = user_data.get("routingProfileId")
if not routing_profile_id:
print("WARNING: User does not have a routing profile attached.")
return False
print(f"SUCCESS: User has routing profile ID: {routing_profile_id}")
return True
if __name__ == "__main__":
# Replace with actual values from your Terraform output or state
USER_ID = "12345678-1234-1234-1234-123456789012"
DOMAIN = os.getenv("GC_OAUTH_DOMAIN", "mypurecloud.com")
try:
token = get_genesys_token()
user_data = get_user_details(USER_ID, token, DOMAIN)
validate_user_routing(user_data)
except Exception as e:
print(f"Validation failed: {e}")
Complete Working Example
Below is a complete, copy-pasteable Terraform module. It creates a routing skill, a routing profile, and a user, linking them correctly according to the v1.35.0 schema.
terraform {
required_providers {
genesyscloud = {
source = "mycon/genesyscloud"
version = ">= 1.35.0"
}
}
}
provider "genesyscloud" {
# Ensure environment variables are set:
# GC_OAUTH_CLIENT_ID
# GC_OAUTH_CLIENT_SECRET
# GC_OAUTH_DOMAIN
}
# 1. Data Source for Default Division
data "genesyscloud_user_division" "default" {
name = "Default"
}
# 2. Create a Routing Skill
resource "genesyscloud_routing_skill" "skill_tech" {
name = "Technical Support"
description = "Handles technical issues"
}
# 3. Create a Routing Profile with the Skill
resource "genesyscloud_routing_profile" "profile_tech" {
name = "Tech Support Agent"
description = "Profile for technical support agents"
skills = [genesyscloud_routing_skill.skill_tech.id]
# Optional: Wrap-up codes
wrapup_code_groups = []
}
# 4. Create the User with the New Schema
resource "genesyscloud_user" "tech_agent" {
name = "Charlie Tech"
email = "charlie.tech@example.com"
division_id = data.genesyscloud_user_division.default.id
password = "CharlieSecure123!"
# Critical: Link the routing profile here
routing_profile_id = genesyscloud_routing_profile.profile_tech.id
# Optional: Set user phone settings
user_phone_settings {
auto_answer = false
call_waiting = true
}
# Optional: Set languages
languages {
language_id = "en-US"
language_order = 1
}
}
# Output the User ID for validation
output "user_id" {
value = genesyscloud_user.tech_agent.id
}
output "routing_profile_id" {
value = genesyscloud_routing_profile.profile_tech.id
}
Common Errors & Debugging
Error: Invalid attribute name: "skills"
What causes it:
In v1.35.0, you can no longer define a skills block directly inside the genesyscloud_user resource. Skills are strictly managed at the genesyscloud_routing_profile level.
How to fix it:
- Remove the
skillsblock from thegenesyscloud_userresource. - Create or update a
genesyscloud_routing_profileresource. - Add the
skillsattribute to thegenesyscloud_routing_profile. - Assign the
routing_profile_idfrom that profile to thegenesyscloud_user.
Code showing the fix:
Incorrect (v1.34.x style):
resource "genesyscloud_user" "bad_user" {
name = "Bad User"
email = "bad@example.com"
division_id = "..."
password = "..."
# THIS CAUSES THE ERROR IN v1.35.0
skills = ["skill-id-123"]
}
Correct (v1.35.0 style):
resource "genesyscloud_routing_profile" "good_profile" {
name = "Good Profile"
skills = ["skill-id-123"]
}
resource "genesyscloud_user" "good_user" {
name = "Good User"
email = "good@example.com"
division_id = "..."
password = "..."
routing_profile_id = genesyscloud_routing_profile.good_profile.id
}
Error: Error: Missing required argument: "division_id"
What causes it:
The provider no longer assumes a default division if one is not explicitly provided. This prevents accidental user creation in the wrong organizational unit.
How to fix it:
Use a data source to fetch the division ID and pass it to the division_id argument.
Code showing the fix:
data "genesyscloud_user_division" "default" {
name = "Default"
}
resource "genesyscloud_user" "fixed_user" {
# ... other args ...
division_id = data.genesyscloud_user_division.default.id
}
Error: 409 Conflict: User with email ... already exists
What causes it:
Terraform attempts to create a user, but an active user with that email already exists in Genesys Cloud.
How to fix it:
- Import the existing user into Terraform state using
terraform import. - Or, delete the existing user in the Genesys Cloud Admin Console (if it is a test user).
Code showing the fix:
# Import existing user by ID
terraform import genesyscloud_user.tech_agent 12345678-1234-1234-1234-123456789012