If you want to know whether an AI product is really running inside a construction company, don't ask about the model. Ask what broke last week.
For us the answer was: a table too wide for the page, umlauts in filenames, and folder rules that have to match a real org chart. None of that is exciting. All of it decides whether a Bauleiter opens BauGPT again on Monday.
Here is what that looked like in the last stretch of work on the BauGPT backend.
A table that doesn't fit on the page
Construction output is table-heavy. Positions, Nachträge, Mengen, deadlines, responsibilities. So when the assistant generates a Word document, some tables are simply wider than a portrait page.
The old behaviour was to rotate the whole file so the widest table would fit. That fixes one table and ruins everything else: the cover page, the text sections, the signature block. Now a table that doesn't fit gets its own rotated section, and everything around it stays exactly as written.
In the same batch: line breaks render properly in OnlyOffice again, the assistant no longer draws ASCII art inside documents, and repeated table header rows are gone.
That last one sounds trivial until you think about where these files go. They get printed. They get attached to an email to the Bauherr. They land in a Bauakte, sometimes with a signature underneath. A header row that appears twice is not a rendering detail at that point, it is the thing someone has to explain in a meeting.
We also shipped Word template autofill: a user brings their own .docx file with placeholders in it, and the model fills those in. This is the version of "AI writes your document" that enterprises actually want. Not our layout, not our fonts. Their Vorlage, their letterhead, their paragraph order that some Prokurist approved years ago.
Umlauts are not an edge case in German construction
Two small fixes from the same week: we now tell the multibyte string helpers explicitly which encoding they are dealing with, and long filenames get cut at a character boundary instead of at a byte count.
Why bother? Because German construction vocabulary is one long stress test for byte-level string handling. Baustellenüberwachung. Nachtragsprüfung. Straßenbauarbeiten. Slice at a fixed number of bytes and you land in the middle of a character, and the user gets a file with a broken symbol where the ü should be.
Nobody files a ticket for that. They just quietly trust the tool a bit less. In an industry where the software reputation baseline is already low, you do not have much of that trust to spend 🙂
Permissions that survive an actual org chart
The second half of the work was access. This is where most "AI for the Bau" demos fall over, because a demo has one user and a real Baufirma has departments, Niederlassungen, external Planer, and a Geschäftsführung that wants some folders read-only.
What we built:
- Team projects: chat folders that span the whole company, with a matrix deciding who can see and edit what.
- Folders you can lock down so that only the owner and an admin are able to put anything in them.
- When a write gets refused, the system now says what actually blocked it instead of failing generically.
- Tenant identity is decided on the server. We stopped trusting a company ID sent by the client, and separately fixed the assistant's access to a user's personal files.
That fourth one is worth sitting with for a second. If your backend takes the tenant ID from whatever the client sends, you do not have multi-tenancy, you have a suggestion. In construction the tenants are often companies bidding against each other on the same Ausschreibung. It has to be settled server side, every request, no exceptions.
Project-level context got the same treatment. Context that existed but never actually reached the chat now lands there properly: the project brief, a memory tier, pinned items. The knowledge was in the system, it just wasn't showing up where the user was typing.
Onboarding is where enterprise deals get won or lost
Rolling out to a construction enterprise usually means importing a whole company at once out of their directory, not a signup form. Which creates a category of bug that never appears in your own testing.
Users who arrive through a bulk import now get a personal folder and an onboarding record like everyone else. Same starting state as someone who signed up by hand.
Single sign-on got an honest failure mode too. It used to crash when someone came in through the personal path and an account for them already existed. Now it hands back an error you can act on. The difference matters because the person hitting that error is an IT admin during rollout week, and they will either forward a useful message to us or forward a stack trace to their boss.
For the same audience, we spelled out in the Active Directory setup steps which Graph permissions are Application-level and which are Delegated. That distinction is the single most common place a Microsoft-side setup stalls. Writing it down in the docs is not engineering work, but it removes a call from the onboarding path.
The assistant should be honest about what it can do
One prompt change: the assistant was getting its own capability list wrong, so we fixed that.
An assistant that overstates itself is worse than one that says "I can't do that yet", especially with a first-time enterprise user who is already sceptical. One wrong claim about what it can read or generate, and they conclude the whole thing is a toy.
Two performance changes on the prompt side: every user now starts from the same cached prefix, and the unchanging text sits in front of the per-user memory blocks. Same idea both times. The parts that never change belong at the front so they can be cached, and the parts that vary per person come after. Put a memory block near the top and you invalidate the cache for everyone.
The connection also stays open now while the model is thinking for longer. On a hard question about a Vertragsklausel, it may be a while before the first token appears, and a UI that looks frozen reads as broken.
The boring safety nets
Document layout is now guarded in CI. That eval suite kept breaking the guard step, so it got its own database. Layout is treated like any other regression risk, because in this product it is one.
Migrations got made idempotent so re-running them is safe, and AI generation spans now carry the company ID, which means we can look at behaviour per customer instead of one undifferentiated blob.
Then there is the identity plumbing on the CRM side, which took four separate fixes. We call identify first and alias afterwards, so merges stop getting dropped. Identify calls that vanish quietly get spotted and repaired. A canary whose result was going straight to /dev/null, and therefore verified nothing, actually reports now. And if a backfill gets started without an end date, you get a warning.
My favourite of the batch: accounts that came in through a directory sync never get replayed into the drip campaign. If a construction company syncs their employees in, those people should not receive a cheerful "welcome, try BauGPT" email sequence. They did not sign up. Their IT department added them. Getting onboarding email into an enterprise inbox is a good way to make the Geschäftsführung regret the rollout.
The point
Every one of these is small. Together they are the actual product.
The model is the easy part now. Anyone can wire one up in an afternoon. What separates an AI tool a Bau company keeps from one they cancel after the pilot is whether the Word document opens correctly, whether the ü survives the filename, whether the folder the Geschäftsführung marked read-only is genuinely read-only, and whether the admin who imported the users gets a clear error instead of a crash.
Ship the boring half. That's where the trust comes from 😎