409 Conflict on evaluationforms during Terraform apply in GitHub Actions

Error: 409 Conflict on /api/v2/quality/evaluationforms followed by a state lock timeout in the S3 backend. The Terraform apply step keeps bombing out when the merge triggers the pipeline. We’re trying to lock down a CI/CD flow that runs terraform plan on every PR and only executes terraform apply once the branch merges to main. The Genesys Cloud provider handles evaluation forms fine locally, but the GitHub Actions runner throws a fit over the state file path and token rotation.

Here’s the workflow we’ve got running:
name: Genesys TF Pipeline
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
plan:
runs-on: ubuntu-latest
if: github.event_name == ‘pull_request’
steps:

  • uses: actions/checkout@v4
  • run: terraform init
  • run: terraform plan -out=tfplan
    apply:
    runs-on: ubuntu-latest
    if: github.event_name == ‘push’
    steps:
  • uses: actions/checkout@v4
  • run: terraform init
  • run: terraform apply tfplan

The apply job doesn’t have access to the tfplan binary from the plan job. Storing it in artifacts breaks the provider cache, and the OAuth token expires before the merge step actually kicks off. We’ve tried passing the state file via environment variables, but the provider drops the connection immediately if the payload lacks the genesys-cloud:write scope. The backend config points to a remote S3 bucket, yet the provider still tries to read the local .tfstate on the runner. It’s a mess. How do you properly chain the plan artifact to the apply step without breaking the OAuth handshake? The evaluation scoring forms need to deploy atomically. Running terraform refresh first just masks the drift instead of fixing it.

The 409 Conflict on /api/v2/quality/evaluationforms usually triggers when the runner attempts a concurrent write without validating the If-Match version header. Terraform tracks the resource version internally but the GitHub Actions runner sometimes desyncs the state backend during token rotation. You need to force a state refresh before the apply step. Add terraform refresh or configure the provider block with ignore_missing_customization = true if version mismatches occur. The S3 lock timeout happens because the previous plan job leaves a stale terraform.tfstate.lock.info file when the pipeline cancels early.

Configure the backend with dynamodb_table = "terraform-locks" instead of relying on the default S3 file lock. Run aws dynamodb delete-item --table-name terraform-locks --key '{"LockID":{"S":"<stale-id>"}}' if the runner hangs. Also check the GENESYS_CLOUD_ACCESS_TOKEN rotation logic. The provider caches the token in memory. When the token expires mid-apply, the API returns 409 because the request signature doesn’t match the current session. Set retry_max = 5 and retry_wait_ms = 3000 inside the genesyscloud provider configuration. This forces exponential backoff instead of immediate failure. The evaluation form payload also requires explicit scorecards array validation before hitting the endpoint. Wrap the terraform apply in a bash script that checks terraform plan -refresh-only first. Just run the refresh step. The state syncs up.

Are you actually pulling the current Version Tag before the apply step runs, or is the runner just guessing at the payload structure? The suggestion above nails the state desync, but you’re missing the version fetch. The 409 Conflict usually hits when the Quality Settings Configuration bypasses the version check entirely. You’ll want to align your pipeline with how the Architect Flow handles state updates, since both rely on strict versioning. Here’s what usually fixes the desync:

  • Run a quick GET on the Evaluation Forms endpoint to grab the latest version string
  • Pass that version directly into the provider block as a custom variable
  • Skip the automatic state lock and let the API handle the retry logic
from purecloudplatformclientv2 import QualityApi, EvaluationForm

# Requires quality:evaluationform:read and quality:evaluationform:write scopes
api = QualityApi(platform_client)
current = api.get_quality_evaluationform(form_id)
payload = EvaluationForm(
 name="Updated Form",
 version=current.version,
 description="Pipeline update"
)
api.put_quality_evaluationform(form_id, payload)

That usually clears the timeout. The API expects a clean version match on every PUT. Honestly, Terraform’s state file gets in the way more than it helps. Usually just a token rotation glitch. You’ll be fine once the version matches.

The 409 hits because the runner tries to overwrite the form without grabbing the latest version tag from /api/v2/quality/evaluationforms. S3 locks up when the pipeline retries with a stale token. Token rotation mid-run kills the session before the If-Match header validates. You’ll need to force a state refresh before the apply step. Drop this into your workflow:

- run: terraform plan -refresh-only
 env:
 GC_TOKEN: ${{ secrets.GC_TOKEN }}

Add lock_timeout = "30s" to your backend block. The provider ignores customizations if the version drifts past two API calls. Snapshotting the state file before the merge catches drift early. [screenshot: state_drift_check.png]

Watch that ETag mismatch on the PUT request. It’ll bite you hard. The pipeline will bail if the version mismatches again.

The documentation notes that the version header must align perfectly before the system’s update process goes through. The refresh command mentioned earlier cleared the lock timeout. The state file synced up properly this time.

Token rotation breaks the version check in a weird way. Does the performance dashboard track these version conflicts as drop-offs? Queue activity metrics usually spot backend hiccups like this. You’ll see the agent performance numbers stabilize once the pipeline runs smoothly. How does this version lock affect the random split node in the flow designer? It’s confusing when the backend state changes but the flow preview stays the same.

The console output stopped right before the final status. The log looks like this:

Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not
be persisted to local or remote state storage.

Randomly Assigned Instance ID: 8f3a2c1d-4b5e-6789-0123-456789abcdef

The form updated anyway. It’s not clear if the ignore_missing_customizations flag is still needed now.