Skip to content
OrtmannConsulting
Back to all projects

Group IT, building materials

Four years of an IT admin portal — one form instead of five consoles

Leaver processing, password resets, licence cleanup and phone numbers run through one internal portal rather than several Microsoft consoles. The job queue is a table in the database that was already there.

Year
2026
Services
Automation
Area
Corporate IT
Role
In-house, employed

Delivered in an employed role for an internal department, not under an external consulting contract. The organisation is not named.

4 yearsGrown in production310 commits since 2022
36Feature modulesaround 100 endpoints
67Tests after the security review

The situation

How leavers, password resets and licence cleanup were handled before the portal is not documented in the project — presumably by hand in the Microsoft consoles, but that is an assumption.

What is evidenced is a later, more specific problem: the leaver process originally ran synchronously inside the web request and hit timeouts as soon as several requests arrived at once. A second documented pain point: actions against the on-premises Active Directory were fire-and-forget — the webhook answered “queued”, and a failed job stayed invisible until somebody happened to check.

The approach

A Flask application with one module per task: offboarding, account and password resets, licence cleanup, Teams phone numbers, device deletion, Copilot management, mailbox access, reporting. Administrators sign in with their normal corporate account; every protected route checks the session server-side.

The defining pattern: anything slow does not run inside the web request. The route validates input, inserts a row with status pending into a queue table, and immediately returns a self-refreshing status page. Timer-triggered functions drain those queues and do the real work against Microsoft Graph, Exchange Online, OneDrive and — through an Automation webhook — the on-premises Active Directory.

Progress is written back to the same row through a consistent lifecycle: pending → processing → completed | partial | failed. Each row records which sub-steps already succeeded, so bounded automatic retries skip what is done rather than repeating it.

Where it could have gone the other way

The existing SQL database became the queue, instead of introducing a message broker.

The commit that decided it is titled simply “move leaver process to queueing”. It converted the offboarding flow from in-request execution to the queue-table-plus-timer-function pattern — and that pattern was then reused for device deletion and phone numbers.

The alternative, a Service Bus or a Celery-style architecture, would have added infrastructure. A plain SQL table instead gives observability through a SELECT, a natural place to record per-step progress, and idempotent retries via the logged completed steps.

The same judgement shows a deliberate stopping point. For the one step that is not safely repeatable — renaming the on-premises account — a confirmed failure is surfaced as partial and handed to a person. The changelog states it outright: no automatic retry, because that step is not idempotent. The system automates aggressively and refuses to retry where a retry could do damage.

The result

310 commits between 2022 and 2026 — not a one-off build, but a system that kept growing in production for four years. 36 feature modules, around 100 endpoints, roughly 10 separately deployed function projects, about 13,700 lines of Python.

In 2026 the codebase went through a documented hardening round — CSRF, header trust, XSS, injection, privilege escalation, plus a deny-list of protected accounts. The same round produced the test suite, 67 cases.

Deliberately out of scope: privileged accounts. A deny-list refuses automation against administrator, service and break-glass accounts, and against the operator’s own account. Only regular users run through. Also deliberately manual: schema changes are SQL scripts, and deployment is a documented checklist with staging and a smoke test.

Stated openly: the function projects have no automated tests, older code paths still build SQL as strings, and there is exactly one maintainer.

Stack

  • Python / Flask
  • Microsoft Graph API
  • Azure SQL
  • Azure Functions
  • Entra ID / MSAL
  • PowerShell

More projects

Group customer service, construction chemicals

2026
11Countries on one platform

One contact centre for eleven countries — one platform, not eleven islands

Customer service across eleven countries over voice and SMS, with data from SAP, Salesforce and the product system surfaced directly in the agent workspace. Country differences are configuration, not separate installations.

  • Twilio Flex
  • Twilio TaskRouter
  • Twilio Studio
  • SAP

Group compliance, construction chemicals

2026
10,872Documents generated

10,872 EU declarations of conformity, not copy-paste

New EU packaging regulation required declarations of conformity in 12 languages for around 900 packaging materials. Instead of filling them in one by one, a pipeline generates and files them in a single run.

  • Python
  • docxtpl / Jinja2
  • Microsoft Word COM
  • Microsoft Graph API

Sales & inside sales, construction chemicals

2026
1 + 3Orchestrator and specialist agents

One orchestrator, three specialist agents for product questions

Instead of one agent with three knowledge sources, a parent agent routes to three specialists — one each for ERP article data, product content and SharePoint documents.

  • Microsoft Copilot Studio
  • Azure AI Search
  • Azure Functions
  • SharePoint

A similar process at your company?

30 minutes, free, no sales pitch. You describe what is costing you time — I tell you honestly whether automation is worth it.