Graph Directory Sync
Scheduled employee directory synchronization for WordPress using Microsoft Graph (users, photos, delta queries) with retry/backoff, failure logging, and a custom read-only table
by redrofigt · github.com/redrofigt/wp-graph-directory-sync · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/redrofigt/wp-graph-directory-sync/archive/refs/heads/main.zipReadme
Graph Directory Sync for WordPress
Scheduled employee-directory synchronization from Microsoft Graph into a custom read-only WordPress table — with delta queries, pagination, exponential-backoff retries, and structured failure logging. Bonus requirement demonstrated: what happens when the API is unavailable.
Scheduler setup
Two WP-Cron events registered on activation (and triggerable manually):
| Event | Schedule | What it does |
|---|---|---|
gds_delta_sync |
Hourly | /users/delta pass using the stored deltaLink — only changed users cross the wire |
gds_full_sync |
Weekly | Fresh sweep with @odata.nextLink pagination + soft-delete reconciliation |
On production, WP-Cron is disabled and both events are triggered by a system cron every 5 minutes:
*/5 * * * * /usr/local/bin/wp --path=/var/www/html cron event run --due-now --quiet
The engine is idempotent (upserts keyed on the Entra object ID) so an overlap between runs is harmless.
Sync execution flow
wp-cron / WP-CLI
│
├─ token: client-credentials grant (cached, expires 5 min early)
│
├─ GET /v1/users/delta?$top=100 ◄── starts from stored deltaLink if present
│ └─ for each page: upsert rows with batch UUID
│ └─ @removed annotations → soft-delete (account_enabled = 0)
│
└─ sweep finished cleanly?
├─ yes → store @odata.deltaLink (atomic resume point)
└─ no → deltaLink NOT advanced → next run resumes where it failed
Failure handling (the bonus demo)
Failure modes and their behavior:
| Failure | Behavior |
|---|---|
| Network timeout / DNS | Retried 4× with jittered exponential backoff (1s→8s ±30%), then logged, pass aborted without advancing the deltaLink |
| HTTP 429 / 503 | Honors Retry-After header, then backoff; logged if exhausted |
| HTTP 401/403/404 | No retry (permanent) — logged with response body, surfaced in wp gds failures |
| Malformed JSON | Logged as gds_bad_json, pass aborted safely |
| Missing credentials | Logged as config_missing, no crash |
All failures funnel through a structured logger (JSON entries, 200-line rotation, error_log() mirror) and fire the gds_sync_failed action for Teams/Slack alerting.
Demo recording script (for the screening answer):
wp gds sync --mode=full→ show the success output and rows appearing on a frontend directory page (shortcode[gds_directory]or a block reading the table)- Break connectivity (e.g. block
graph.microsoft.comat the firewall / invalid tenant ID) wp gds sync --mode=delta→ show the retry/backoff messages, thenwp gds failuresshowingnetwork_errorentries with timestamps- Restore connectivity →
wp gds sync --mode=delta→ success; show that only the failed pass's window needed re-syncing
Schema
CREATE TABLE wp_gds_employees (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_oid VARCHAR(64) NOT NULL, -- Entra object ID (stable key)
display_name VARCHAR(191) NOT NULL,
given_name VARCHAR(100) NULL,
surname VARCHAR(100) NULL,
job_title VARCHAR(191) NULL,
department VARCHAR(191) NULL,
office_location VARCHAR(191) NULL,
mail VARCHAR(191) NULL,
business_phones TEXT NULL,
mobile_phone VARCHAR(64) NULL,
photo_url VARCHAR(255) NULL, -- CDN/Azure Blob URL after caching
photo_blob LONGBLOB NULL, -- optional local cache
presentity_type VARCHAR(32) NOT NULL DEFAULT 'employee',
account_enabled TINYINT(1) NOT NULL DEFAULT 1, -- soft delete
graph_created DATETIME NULL,
last_synced_at DATETIME NOT NULL,
sync_batch_id VARCHAR(36) NOT NULL, -- per-batch reconciliation key
PRIMARY KEY (id),
UNIQUE KEY tenant_oid (tenant_oid),
KEY dept_name (department, display_name),
KEY sync_batch (sync_batch_id),
KEY enabled_name (account_enabled, display_name)
);
WP-CLI
wp gds sync --mode=delta # run the incremental sync now
wp gds sync --mode=full # run a full reconciliation sweep
wp gds failures --limit=20 # tail the structured failure log
Security / least privilege
- App-only token via client credentials; secret injected from environment (Key Vault in production), never stored in the DB
- Graph application permission required:
User.Read.All(nothing broader) - The custom table is read-only by convention: no admin UI writes to it; only the sync engine writes
- Soft deletes only — no destructive SQL against directory history