Genesys Cloud User Resource Schema Migration: Handling the v1.35.0 Breaking Change

Genesys Cloud User Resource Schema Migration: Handling the v1.35.0 Breaking Change

What You Will Build

  • A Terraform configuration that correctly provisions a Genesys Cloud user using the updated genesyscloud_user resource schema introduced in provider version 1.35.0.
  • This tutorial uses the Genesys Cloud Terraform Provider (HCL) and the Genesys Cloud REST API for validation.
  • The primary language covered is HashiCorp Configuration Language (HCL) with supporting Python code for API verification.

Prerequisites

  • Terraform: Version 1.5.0 or later.
  • Genesys Cloud Terraform Provider: Version 1.35.0 or later.
  • OAuth Credentials: A Genesys Cloud OAuth client with the following scopes:
    • user:read
    • user:write
    • userprofile:read
    • userprofile:write
    • routing:queue:read (if assigning to queues)
  • Python Environment: Python 3.9+ with requests library for API validation steps.

Authentication Setup

Before modifying state or code, you must authenticate Terraform against the Genesys Cloud environment. The provider supports two methods: environment variables or inline configuration. For security in CI/CD pipelines, environment variables are recommended.

Set the following environment variables:

export GENESYS_CLOUD_REGION="us-east-1"
export GENESYS_CLOUD_OAUTH_CLIENT_ID="your-client-id"
export GENESYS_CLOUD_OAUTH_CLIENT_SECRET="your-client-secret"

If you are using a service account with a private key file, the configuration differs slightly, but for this tutorial, we assume standard OAuth2 client credentials flow.

Initialize the Terraform provider in your main.tf:

terraform {
  required_providers {
    genesyscloud = {
      source  = "genesyscloud/genesyscloud"
      version = ">= 1.35.0"
    }
  }
}

provider "genesyscloud" {
  # The provider will automatically use GENESYS_CLOUD_* environment variables
  # If you prefer inline, use:
  # oauth_client_id     = var.oauth_client_id
  # oauth_client_secret = var.oauth_client_secret
  # region              = var.region
}

Implementation

Step 1: Understand the Breaking Change

In versions prior to 1.35.0, the genesyscloud_user resource allowed certain attributes to be defined at the root level that were subsequently deprecated or moved to nested blocks. Specifically, the handling of user profiles, routing skills, and phone numbers underwent a schema normalization.

The critical breaking change in v1.35.0 involves the removal of direct phone_numbers and routing_skills attributes from the root level of the genesyscloud_user resource. These must now be managed via separate resources (genesyscloud_user_phonenumber and genesyscloud_user_routing_skill) or via a consolidated nested block if supported by the specific sub-resource update.

However, the most significant immediate impact is the restructuring of the home_station and phone_number assignment logic. You can no longer assign a phone number directly inside the user resource. You must create the user, then attach the phone number.

If you attempt to use the old schema:

# DO NOT USE THIS - This will fail in v1.35.0+
resource "genesyscloud_user" "example_user" {
  name = "Test User"
  email = "test.user@genesys.com"
  phone_numbers = ["+12025550199"] # ERROR: Argument "phone_numbers" is not expected here.
}

You will receive a plan error:

Error: Unsupported argument

  on main.tf line 5, in resource "genesyscloud_user" "example_user":
   5:   phone_numbers = ["+12025550199"]

An argument named "phone_numbers" is not expected here.

Step 2: Define the New User Resource

To resolve this, you must define the user without telephony attributes. The core genesyscloud_user resource now focuses strictly on identity, authentication, and basic profile data.

resource "genesyscloud_user" "example_user" {
  name  = "Alice Developer"
  email = "alice.developer@genesys.com"

  # Division is optional but recommended for multi-division orgs
  division_id = var.default_division_id

  # User profile data that remains at the root level
  user_type = "AGENT"

  # Password must be provided for non-SAML users
  # In production, use a random_password resource or secret manager
  password = "SecureP@ssw0rd123!"

  # First name and last name are required
  first_name = "Alice"
  last_name  = "Developer"

  # Address information (optional)
  address {
    address_line_1 = "123 Tech Lane"
    city           = "Washington"
    state_province = "DC"
    postal_code    = "20001"
    country        = "US"
  }
}

Key changes to note:

  1. phone_numbers is removed.
  2. routing_skills is removed.
  3. home_station is removed.

Step 3: Attach Phone Numbers Separately

You must now use the genesyscloud_user_phonenumber resource to attach a phone number to the user. This resource requires the user_id from the previous step and a phone_number object.

First, ensure you have a phone number resource or a known phone number ID/Number in your org. For this example, we assume a phone number has already been provisioned or is being provisioned in parallel.

# Provision a phone number (optional, if not already existing)
resource "genesyscloud_phone_number" "example_phone" {
  phone_number = "+12025550199"
}

# Attach the phone number to the user
resource "genesyscloud_user_phonenumber" "example_user_phone" {
  user_id = genesyscloud_user.example_user.id

  # The phone_number_id must reference an existing phone number resource
  phone_number_id = genesyscloud_phone_number.example_phone.id

  # Optional: Set as primary
  primary = true
}

This separation of concerns ensures that the user identity is decoupled from the telephony configuration, which is a significant architectural shift in the Terraform provider.

Step 4: Assign Routing Skills Separately

Similarly, routing skills are no longer assigned within the user resource. You must use genesyscloud_user_routing_skill.

# Assume a skill already exists. If not, create it.
data "genesyscloud_routing_skill" "existing_skill" {
  name = "Support Tier 1"
}

# Assign the skill to the user
resource "genesyscloud_user_routing_skill" "example_user_skill" {
  user_id = genesyscloud_user.example_user.id
  skill_id = data.genesyscloud_routing_skill.existing_skill.id

  # Proficiency level (0-100)
  proficiency = 80

  # Optional: Assign to specific queues if needed via separate resources
}

Step 5: Validate with Python API Call

To verify that the Terraform state matches the actual Genesys Cloud API state, you can write a Python script to fetch the user details and check for the presence of phone numbers and skills.

import requests
import json
import os

def get_access_token():
    """
    Retrieves an OAuth2 access token from Genesys Cloud.
    """
    oauth_url = "https://api.mypurecloud.com/oauth/token"
    client_id = os.environ.get("GENESYS_CLOUD_OAUTH_CLIENT_ID")
    client_secret = os.environ.get("GENESYS_CLOUD_OAUTH_CLIENT_SECRET")
    region = os.environ.get("GENESYS_CLOUD_REGION", "us-east-1")
    
    # Adjust base URL based on region
    if region == "us-east-1":
        base_url = "https://api.mypurecloud.com"
    elif region == "eu-west-1":
        base_url = "https://api.eu.mypurecloud.com"
    else:
        raise ValueError("Unsupported region")

    response = requests.post(
        f"{base_url}/oauth/token",
        data={
            "grant_type": "client_credentials"
        },
        auth=(client_id, client_secret),
        headers={"Content-Type": "application/x-www-form-urlencoded"}
    )

    if response.status_code != 200:
        raise Exception(f"Failed to get token: {response.text}")
    
    return response.json()["access_token"]

def get_user_details(access_token, user_email):
    """
    Fetches user details by email using the Genesys Cloud API.
    """
    base_url = "https://api.mypurecloud.com"
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json"
    }

    # Search for user by email
    search_url = f"{base_url}/api/v2/users?email={user_email}"
    response = requests.get(search_url, headers=headers)

    if response.status_code != 200:
        raise Exception(f"Failed to fetch user: {response.text}")
    
    users = response.json()["entities"]
    if not users:
        raise Exception("User not found")
    
    user = users[0]
    return user

def validate_user_schema(access_token, user_email):
    """
    Validates that the user exists and checks for phone numbers/skills.
    """
    try:
        user = get_user_details(access_token, user_email)
        print(json.dumps(user, indent=2))
        
        # Check for phone numbers
        phone_numbers = user.get("phoneNumbers", [])
        print(f"Phone Numbers Assigned: {len(phone_numbers)}")
        for pn in phone_numbers:
            print(f"  - ID: {pn.get('id')}, Number: {pn.get('phoneNumber')}")

        # Check for routing skills
        routing_skills = user.get("routingSkills", [])
        print(f"Routing Skills Assigned: {len(routing_skills)}")
        for rs in routing_skills:
            print(f"  - Skill ID: {rs.get('skillId')}, Proficiency: {rs.get('proficiency')}")

        return True
    except Exception as e:
        print(f"Error validating user: {e}")
        return False

if __name__ == "__main__":
    token = get_access_token()
    validate_user_schema(token, "alice.developer@genesys.com")

Complete Working Example

Here is the complete main.tf file that implements the new schema requirements.

terraform {
  required_providers {
    genesyscloud = {
      source  = "genesyscloud/genesyscloud"
      version = ">= 1.35.0"
    }
  }
}

provider "genesyscloud" {
  # Credentials loaded from environment variables
}

variable "default_division_id" {
  description = "The ID of the default division for the user"
  type        = string
  default     = null # Uses default division if null
}

# 1. Create the User (Identity Only)
resource "genesyscloud_user" "alice_dev" {
  name       = "Alice Developer"
  email      = "alice.developer@genesys.com"
  first_name = "Alice"
  last_name  = "Developer"
  user_type  = "AGENT"
  
  # Password is required for non-SAML users
  password   = "SecureP@ssw0rd123!"

  address {
    address_line_1 = "123 Tech Lane"
    city           = "Washington"
    state_province = "DC"
    postal_code    = "20001"
    country        = "US"
  }

  # Division is optional
  dynamic "division_id" {
    for_each = var.default_division_id != null ? [var.default_division_id] : []
    content {
      value = division_id.value
    }
  }
}

# 2. Provision a Phone Number (If not already existing)
resource "genesyscloud_phone_number" "alice_phone" {
  phone_number = "+12025550199"
  
  # Wait for the phone number to be activated
  depends_on = [genesyscloud_user.alice_dev]
}

# 3. Attach Phone Number to User
resource "genesyscloud_user_phonenumber" "alice_phone_attachment" {
  user_id         = genesyscloud_user.alice_dev.id
  phone_number_id = genesyscloud_phone_number.alice_phone.id
  primary         = true
}

# 4. Assign Routing Skill (Assuming skill exists)
# Note: In a real scenario, you might use a data source or create the skill
data "genesyscloud_routing_skill" "support_tier1" {
  name = "Support Tier 1"
}

resource "genesyscloud_user_routing_skill" "alice_skill" {
  user_id     = genesyscloud_user.alice_dev.id
  skill_id    = data.genesyscloud_routing_skill.support_tier1.id
  proficiency = 90
}

Common Errors & Debugging

Error: Unsupported argument “phone_numbers”

What causes it:
You are using genesyscloud_user with phone_numbers attribute after upgrading the provider to v1.35.0+.

How to fix it:
Remove the phone_numbers block from the genesyscloud_user resource. Create a genesyscloud_phone_number resource if necessary, and then use genesyscloud_user_phonenumber to link them.

Code showing the fix:

# BEFORE (Broken in v1.35.0)
resource "genesyscloud_user" "bad_user" {
  name = "Bad User"
  phone_numbers = ["+12025550199"]
}

# AFTER (Correct)
resource "genesyscloud_user" "good_user" {
  name = "Good User"
}

resource "genesyscloud_user_phonenumber" "good_user_phone" {
  user_id = genesyscloud_user.good_user.id
  phone_number_id = genesyscloud_phone_number.existing.id
}

Error: 409 Conflict on Phone Number Assignment

What causes it:
You are trying to assign a phone number to a user, but the phone number is already assigned to another user or resource.

How to fix it:
Ensure the phone number is unique or unassigned. Check the Genesys Cloud admin console or use the API to verify the current assignment.

# Check current assignment via API
def check_phone_assignment(access_token, phone_id):
    base_url = "https://api.mypurecloud.com"
    headers = {"Authorization": f"Bearer {access_token}"}
    response = requests.get(f"{base_url}/api/v2/telephony/providers/edges/phonenumberassignments/{phone_id}", headers=headers)
    if response.status_code == 200:
        print(json.dumps(response.json(), indent=2))
    else:
        print(f"Error: {response.status_code}")

Error: 400 Bad Request on User Creation

What causes it:
Missing required fields such as first_name, last_name, or email. The v1.35.0 schema is stricter about validation.

How to fix it:
Ensure all required fields are present in the genesyscloud_user resource.

resource "genesyscloud_user" "valid_user" {
  name       = "Valid User"
  first_name = "Valid" # Required
  last_name  = "User"  # Required
  email      = "valid.user@genesys.com" # Required
  user_type  = "AGENT" # Required
  password   = "StrongPassword123!" # Required for non-SAML
}

Official References