Back to Playbooks

Zendesk and Salesforce Drift Audit

Maintain
~45 minutes

Zendesk organization external_ids, user memberships, and ticket attribution drift from Salesforce over time. You need to audit the whole surface, review grouped exceptions by account, and apply only 4 safe Zendesk-side repairs — with receipts for every attempted, applied, blocked, and skipped action.

The Slack Message

SO
Head of Support Operations10:42 AM

CSMs are getting escalations because tickets are landing on the wrong account team. I already found 3 customers with duplicate Zendesk orgs, and some end-users are assigned to the wrong org entirely. Can you figure out how bad this actually is across every account and tell me what's safe to fix in Zendesk without touching Salesforce?

The Prompt

Kicked off from the terminal. The demo flag lets ops see the artifact shape before connecting a live Zendesk token.

We think Zendesk is drifting from Salesforce for some customers.
I need to:
1. Audit every Salesforce account against its Zendesk org, users, and tickets.
2. Get one grouped triage file I can share with support ops.
3. See the fix plan with which actions are safe-to-apply and which are blocked.
4. Apply only the safe Zendesk-side actions. Never touch Salesforce. Never mutate tickets.

Use --demo first so I can see the artifact shape before we connect the live Zendesk.

The grouped exception file is the product

This is not “magic sync.” Gremlin detects drift across 12 issue codes, groups the exceptions by Salesforce account in exceptions_by_account.csv, and prepares a fix_plan.json with a stable plan_hash. Audit → review → dry-run → apply. Only 4 safe Zendesk-side actions ever run. Salesforce and tickets are never mutated.

Core Workflow

Audit, detect, triage, dry-run, apply. Every step is supervised.

1

Audit drift

Run g-gremlin zendesk drift audit against a Salesforce reference CSV and Zendesk credentials (or --demo for zero-auth fixtures).

2

Detect across 3 layers

12 categorized issue codes: 6 org-level, 4 user-level, 2 ticket-level. Ticket codes are detection-only.

3

Grouped triage

exceptions_by_account.csv: one row per Salesforce account with prepared_action_count, blocked_action_count, and top_blocked_reasons.

4

Dry-run apply

Revalidate preconditions against the hashed fix_plan.json. No writes unless --apply is passed with a matching --confirm-plan-hash.

5

Live apply with caps

Only 4 safe actions run: set_org_external_id, add_org_domain, assign_user_to_org, move_user_to_org. Caps enforced up front.

After you paste this prompt, Claude Code:

"Three layers to audit: orgs, users, and tickets. The demo loader uses bundled fixtures that mirror a real run, so I'll start there before anyone connects a live Zendesk token."

"Step 1: g-gremlin zendesk drift audit --demo. Step 2: review exceptions_by_account.csv. Step 3: identify blocked accounts and why. Step 4: dry-run the fix plan. Step 5: live apply the safe actions with caps. Step 6: rerun the audit to prove the drift count dropped."

Running
$ g-gremlin zendesk drift audit --demo
Loaded 9 accounts, 11 orgs, 24 users, 142 memberships, 487 tickets. Detected 12 issues (5 org, 4 user, 3 ticket). Prepared 5 safe actions. Blocked 4. Artifacts written to ./artifacts/zendesk_drift/20260417_221530Z_audit/. plan_hash: 8c5d4d4d4b0f1a...

"5 prepared actions, 4 blocked. The blockers are 2 duplicate_external_id, 1 domain_belongs_to_other_org, and 1 user_domain_ambiguous. Those cannot be auto-fixed. They need a human to resolve the ambiguity in Zendesk before any safe action can be prepared for those accounts."

Running
$ open ./artifacts/zendesk_drift/20260417_221530Z_audit/exceptions_by_account.csv
6 account rows. 2 safe-to-apply (ACME001, ACME004). 3 blocked (ACME002 duplicate_external_id, ACME003 domain_belongs_to_other_org, ACME005 user_domain_ambiguous). 1 ticket-only (ACME006, no write actions generated).

Audit done. 5 safe actions ready. 2 accounts need Zendesk-side review before their drift can be repaired. 1 account is ticket-only and does not generate writes.

exceptions_by_account.csv

One row per Salesforce account. Support ops opens this file in a spreadsheet, uses the prepared_action_count and blocked_action_count columns to decide which accounts go through, and resolves the blocked ones in Zendesk before re-running the audit.

sf_account_idaccount_nametotalpreparedblockedtop_issue_codestop_blocked_reasons
001ACME001Acme Healthcare110org_missing_external_id
001ACME002Acme Labs302org_duplicate_external_idduplicate_external_id
001ACME003Acme Biotech101org_domain_attached_elsewheredomain_belongs_to_other_org
001ACME004Acme Retail220org_missing_expected_domain, user_missing_expected_membership
001ACME006Acme Studios200ticket_org_mismatch

Green rows are safe-to-apply. Red rows are blocked in Zendesk until ops resolves the ambiguity. Yellow rows are ticket-only and never generate write actions.

After you paste this prompt, Claude Code:

"Support ops reviewed the 2 duplicate_external_id cases in Zendesk and picked the surviving orgs. The ambiguous domain and user cases are assigned to a human. Now the 5 prepared actions should all still pass preconditions. Dry-run first to re-validate."

Running
$ g-gremlin zendesk drift apply \ --plan ./artifacts/zendesk_drift/20260417_221530Z_audit/fix_plan.json
Dry-run: 5 attempted. 5 would apply. 0 blocked. 0 failed. Mode: dry_run. apply_receipt.json written with per-action simulated results.

"Dry-run clean. Applying live with --apply and --confirm-plan-hash. Caps are --max-actions 25 and --max-per-account 3 by default."

Running
$ g-gremlin zendesk drift apply \ --plan ./artifacts/zendesk_drift/20260417_221530Z_audit/fix_plan.json \ --apply \ --confirm-plan-hash 8c5d4d4d4b0f1a... \ --max-actions 25 \ --max-per-account 3
Applied 4 of 5. 1 skipped (precondition_changed_user_already_member on ACME008). 0 blocked. 0 failed. apply_receipt.json written. Mode: apply.

4 Zendesk-side repairs applied. 1 stale row skipped explicitly. 0 Salesforce writes (by design). 0 ticket mutations (by design). Full receipt in apply_receipt.json with source_plan_hash cross-check.

Safety Guarantees

Dry-run by default. Live apply requires both --apply and --confirm-plan-hash.
Only 4 Zendesk-side actions are ever applied live. Salesforce is never written to.
Ticket attribution issues are detection-only. No ticket mutation, ever.
Global cap --max-actions 25 and per-account cap --max-per-account 3, enforced up front.
Preconditions re-checked at apply time. Stale rows skipped with an explicit reason.
Personal email domains (gmail, outlook, yahoo, icloud, hotmail, protonmail) are suppressed from user drift detection.
Accounts blocked by duplicate_external_id remain review-only. No safe action is ever prepared for them.
Every run writes receipt.json and apply_receipt.json with counts, statuses, and blocked reasons.

Requirements

Zendesk API token

Read + narrow write scope (orgs, users, memberships)

Required

Salesforce reference CSV

sf_account_id, account_name, and a domain source

Required

Gremlin CLI with zendesk drift

init, audit, trace, apply commands

Required

--demo mode for dry-runs

Works with zero auth using bundled fixtures

Optional

Results (demo run)

12
Issues detected
5
Safe actions prepared
4
Blocked actions
4
Live actions applied
0 Salesforce writes

By design. Salesforce stays read-only reference input.

0 ticket mutations

Ticket issues are detected but never trigger writes.

Try This Workflow

Start with the demo. No Zendesk token and no Salesforce connection required to see the full artifact set.

Related guide: How to Integrate Salesforce and Zendesk Without Breaking Ownership and Reporting