☀️ The Honeymoon Was Nice While It Lasted
I was in the honeymoon period of my job at the Supreme Council for Economic Affairs and Investment — SCEAI Qatar. That phase every IT person knows: the new environment is still fresh, you're still learning how everything fits together, and the big fires haven't found you yet.
I had just come off fixing the Windows lockscreen pipeline issue for the entire organisation — an Intune headache that had me digging into Windows internals nobody usually touches. That one's a whole other story. But after that, things had settled back into the comfortable rhythm of IT: creating users, granting access, installing software on servers, managing end devices through Intune, enforcing compliance policies, helping the blue team trace suspicious activity on systems, service accounts — and occasionally, quietly, my own test runs in production. (Approved work. All of it. Mostly.)
Then one day, our software architect called me over.
Part 2 — The Problem📋 "The Address Book Isn't Working."
He pulled up a screen and showed me our employee address book — a page hosted on SharePoint that lets staff look up their colleagues' contact details. Names, job titles, departments, phone numbers, office locations. Not personal stuff — just the authorised, work-related information every organisation needs its people to find quickly.
Now, if you work in IT, you already know what your first question is when someone says something "isn't working." What exactly isn't working? Because "not working" in IT could mean a hundred different things.
In this case: the frontend was fine. The page loaded. The design was intact. But the backend — the sync between the employee data in Entra ID and the SharePoint list that powered the page — had quietly stopped doing its job. New employees were invisible. Changed details were stale. The address book was showing the world as it was, not as it is.
You might be wondering: why is a sysadmin the one fixing a data pipeline? Fair question. The honest answer is — I didn't even ask it. The architect showed me the problem, and my brain immediately went to how do I fix this, not is this my job. PowerShell is a scripting language. Automation is in a sysadmin's world. The architecture wasn't some React frontend or backend service — it was a runbook. That's sysadmin territory as far as I'm concerned.
After a quick briefing from him on how the pipeline was supposed to work, I started looking at the legacy code. And that's where things got interesting.
Part 3 — The Legacy Code🔍 Looking at What Broke — and Why We Couldn't Just Fix It
The first thing I checked was the runbook output logs. Right there, screaming at me in red:
Invoke-RestMethod:
Line | 100 | … hotoBytes = Invoke-RestMethod -Uri $photoUri -Headers $headers …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| {"error":{"code":"ImageNotFound","message":"Exception of type
| 'Microsoft.Fast.Profile.Core.Exception.ImageNotFoundException'
| was thrown."}}
Every user. Same error. The script was trying to fetch profile photos from Entra. And out of our 100+ employees, only 2 people had actually uploaded a photo. For everyone else — crash. Except... it wasn't actually crashing. Looking more carefully at the logs, the runbook was continuing after the error. So the photo retrieval wasn't killing it.
That was the first sign that the obvious error wasn't the real problem. The real problems were deeper. And once I dug into the code properly, they were everywhere:
At this point I had a decision to make. I could try to patch the existing code — fix the photo issue, add pagination, untangle the logic. Or I could scrap it and start fresh with a properly designed script.
Decision made: start completely fresh.
Part 4 — The Right Way to Start🧪 New Group. New List. New Site. Nobody Touches Production.
Before writing a single line of new code, I did the responsible thing: got the right access, created a safe sandbox, and informed management I was taking this on. A new Entra group called "Address" — just the IT department, 6 people including me. A new SharePoint list. A new test workspace site. The production environment was not going anywhere near this until I was confident.
And that caution wasn't just process — it was personal. If I broke the existing address book, even in its broken state, that's an escalation. Top management notices when the intranet stops working. I wasn't going to be the reason for that conversation.
I built the new script module by module, testing each one in isolation before connecting them. Authentication first — just get a token and confirm it works. Then read the group and list the members. Then fetch individual user details. Then try writing one record manually to SharePoint. Then try a loop. One step at a time. No assumptions.
This is also where the AI tools became essential. I'm not a developer. I know what I want the code to do — I can describe the logic, the requirements, the expected behaviour — but writing clean PowerShell that talks to Graph API and SharePoint simultaneously is not something I was going to get right on my own. So I used what was available:
👤 "Wait — I Need a Separate API Call Just for the Manager?"
The first real challenge hit when I tried to include manager names in the sync. Seems simple — every employee has a manager, the manager's name is part of the user's profile, so just read it along with everything else, right?
Wrong.
Microsoft Graph doesn't return a user's manager as part of the standard user object. You have to make a dedicated, separate API call for each employee just to get their manager's display name. That means if you have 100 employees, you're making 100 extra API calls just for managers — on top of everything else.
Imagine you're building a contact list and you ask HR for everyone's details. They give you name, email, phone, department — but for the manager's name, you have to go and ask a completely separate office. For every single person. One by one. That's what Graph API requires for manager lookups.
The fix: wrap every manager fetch in a try/catch block. If it succeeds, great — we have a manager name. If it fails (maybe the user has no manager, or the API hiccups), we catch the error, log it quietly, and move on with an empty manager field. The sync doesn't stop for one missing manager.
Small fix in code, big lesson in thinking: always expect partial failures in API work, and design your error handling before you need it.
Part 6 — Second Challenge🖼️ The Profile Photo Trap
The image handling came next. The legacy code had the photo fetch baked into the main sync loop — and as we'd seen from the original error logs, it was taking down users whenever the photo didn't exist. Which was most of the time.
The fix here was structural: decouple the photo logic from the user sync entirely. Try to get the photo. If it exists, handle it. If it doesn't — don't care. Don't fail. Don't retry. Just skip it and carry on. The frontend already had a fallback avatar for users with no photo. So the script just needed to stop treating a missing profile image like a fatal error.
What sounds like a simple fix took time because the original code had these two concerns so deeply tangled together. Untangling them cleanly — making sure the photo failure path couldn't accidentally corrupt the user data path — that required careful thought and quite a few test runs to confirm.
Part 7 — Third Challenge🔤 Custom Security Attributes — The Arabic Problem
Just when the common fields were working smoothly, the requirements got more complex.
Our address book needed to be bilingual — English and Arabic. There's a language toggle on the site. When a user switches to Arabic, every field should display in Arabic: name, job title, department, manager, office location. All of it.
The English data lives in the standard Entra ID user profile. The Arabic data? It's stored in something called Custom Security Attributes — a Microsoft feature that lets organisations add their own extra fields to user objects in Entra. Think of it like adding custom columns to the master employee spreadsheet in the cloud.
And here is where the payload refused to cooperate. The script was reading the custom attributes correctly — or so it seemed — but when it tried to write those values into the SharePoint list, the payload kept failing. The data wasn't landing as expected. Fields weren't populating. Some calls returned errors, others returned silent emptiness.
The root cause turned out to be a combination of things: the custom attributes needed a different API call (Graph SDK, not just REST), the payload structure had to be built differently, and the field mapping between Entra's attribute names and SharePoint's internal column names had to be exact — no guessing, no assumptions.
LineManagerEn — or it might not. You have to query the schema to find out. Any mismatch = 400 Bad Request, with no helpful explanation of which field is wrong.The fix involved querying the actual column schema from SharePoint to get every column's exact internal name, then carefully mapping each Entra attribute to the correct SharePoint field. Once that mapping was right, the Arabic fields started populating correctly. It wasn't glamorous work — it was painstaking, field-by-field verification. But it worked.
"Engineers don't quit when it's hard. They redesign until it works. Redesign your approach, not your dream."
🔄 Wait — I Was Solving the Wrong Problem?
After two weeks, I had something working. All fields — English and Arabic — syncing correctly from the test Entra group to the test SharePoint list. The logic was solid: create new users, update changed ones, skip unchanged ones, remove stale entries. I was proud of it.
I showed the architect.
And that's when I realised I had misunderstood the original requirement.
I thought he wanted a completely new address book — new frontend, new design, new everything. So I had been thinking about the UI too. I had even started sketching a cleaner, more modern design. But what he actually needed was much simpler: just make the sync work. The production frontend already existed. The production SharePoint list already existed. He just needed the pipeline — the runbook — to reliably push the right data into the existing list.
Clarify the actual requirement before building. Not the requirement you think you heard. Not the one that sounds more interesting to build. The actual one. A five-minute conversation at the start could have saved me some of that frontend thinking — but in fairness, the extra understanding of the architecture I gained didn't go to waste either.
This also meant the scope became cleaner. My task was now clear: take my working test script, point it at the production Entra group "Address Book" and the production SharePoint list "Address List," and make it run reliably on a schedule.
Should be straightforward, right?
Part 9 — Production Day🚨 It's Just a Config Change. What Could Go Wrong.
Two weeks of testing. Everything verified. Two days after showing the architect my working test run, I got the green light to switch to production.
I changed two values in the script: the Entra group ID, and the SharePoint list ID. That's it. Everything else stays exactly the same. The code that worked flawlessly in test should work identically in production.
Except it didn't.
✅ [OK] Auth OK --- STEP 1: Reading group members --- Group members found: 104 --- STEP 2: Fetching user details from Entra --- [OK] User1 — custom attrs loaded [OK] User2 — custom attrs loaded . . . User objects built: 104 --- STEP 3: Reading SharePoint list into memory --- SharePoint items loaded: 5 --- STEP 4: Building lookup + detecting duplicates --- Unique emails in SP: 5 | Duplicates to remove: 0 --- STEP 5: Syncing Entra → SharePoint --- === FIRST USER JSON PAYLOAD === . . === END PAYLOAD === [FAIL] CREATE FAILED for User@domain.com: Response status code does not indicate success: 400 (Bad Request). =========================
Every single user failing. 400 Bad Request — which is the API's way of saying "your request is malformed" without telling you exactly what's wrong.
Five hours. That's how long I spent debugging this in production — which, for the record, was exactly where I said I wouldn't be working. The irony was not lost on me.
The debugging was systematic. Authentication? Fine — the token was obtained successfully. Graph API connectivity? Fine — users were being fetched. The issue was in the write step — the moment the script tried to create or update records in the production SharePoint list.
After 5 hours of isolating variables, testing individual fields, comparing the test list schema against the production list schema — the culprit revealed itself.
One field. LastSync.
My script included a LastSync field — a timestamp that gets written every time a user record is created or updated, so you can see when each entry was last touched. In the test SharePoint list, I had created that column myself. It existed. It worked.
In the production SharePoint list — the one I had no permission to edit — that column didn't exist. So every time the script tried to write LastSync to production, SharePoint returned 400: field not recognised. And because the entire payload was rejected, none of the other fields went through either.
LastSync field from the payload entirely. Stop trying to write a column that doesn't exist in production. I couldn't add the column — I didn't have that permission. So I removed it from the code instead. Not the elegant solution. But the working one.With that one line removed from the payload, I ran the script again.
💬 What I Actually Learned
I know this is legacy architecture. I know for experienced developers, some of these problems sound small. I know many people could have done this faster.
But here's what I didn't know before this project started: I also can do this.
I can write code. I can debug a pipeline. I can redesign the logic when the original design isn't working. I can rewrite something from scratch and deliver it. That wasn't in my mental model of my own capabilities before those three weeks.
The AI tools were essential — but not in the way people sometimes imagine. They didn't hand me a finished product. They were collaborators. I brought the domain knowledge: what Entra is, how SharePoint works, what the sync needed to do, what was acceptable to break and what wasn't. They brought the syntax and the debugging suggestions. That combination — human context plus AI capability — is where the real output came from.
And the big lesson underneath all of it:
Don't set boundaries on what your job "is." The moment you decide something isn't your scope, you stop learning. I'm a sysadmin. I fixed a data sync pipeline. I rewrote a PowerShell runbook. I debugged Graph API payloads and SharePoint column schemas. None of that is in a classic sysadmin job description. All of it was something I needed to be able to do.
Effort always pays off, regardless of destiny.
"You can close the laptop, turn off the monitor and lie down in pitch black darkness... But your brain refused to shut down. It keeps compiling the code, running the logic loops and trying to fix the bug in your dreams."