← Work
// automation & AI system · for real-estate

An empty room, furnished in ~33 seconds

Raw listing photos go in, fully-staged interiors come out. It replaces a €150–400, multi-day photo shoot with a pipeline that runs while the agent is still on the phone.

~33 sper photo
~175listings per month
53n8n nodes

The problem

An empty apartment photographs badly and sells slowly. Staging one for a shoot costs between €150 and €400 and takes days to schedule, for a listing that might sell in a week.

So most empty flats simply went online empty, and underperformed.

What I built

An internal web form: upload a photo, pick the room and a furnishing style, get back a staged interior in about thirty seconds. Fifty-three nodes behind a shared access code, with a daily quota the manager can change from a dashboard, and a page showing exactly what it has cost this month.

  • Four actions (furnish, declutter, empty, upscale), detected from explicit fields, never from free text.
  • Thirty furnishing styles, filtered by room, so a kitchen never offers a bedroom style.
  • An optional floor plan passed as a second image, so the model respects the real layout.
  • A before/after slider, and a copy-image button that puts a PNG straight on the clipboard.
Empty room before staging
AI-furnished room after staging
BEFORE AFTER · AI

How it works

INPUT Agent uploads photo PAYLOAD Web form room + style ✦ AI ENGINE · ~33s Image model furnish · declutter · empty + camera lock STORAGE Shared Drive stores the render OUTPUT Result page before / after slider CONSTRAINTS Prompt builder pinned to the end ATOMIC 🔒 Job ledger reserved up front Usage & cost daily quota · editable

The quota is reserved before generation, never after.

Under the hood

PITFALL

The model kept redecorating the ceiling

Early renders moved the camera and invented a chandelier. The client wanted their room, furnished. Not a different, nicer room.

FIX A block of hard constraints pinned to the very end of the prompt, because image models weight the end most heavily, plus reframing the whole task as “an in-place photo edit, not a new render”. It is prompt steering, not a guarantee: when a render still drifts, you regenerate.

PITFALL

A random job ID that sometimes said “4K”

The action was detected by scanning the whole request payload for keywords. Job IDs were base-36 random strings, so roughly two percent of them contained 4K or HD, and silently triggered a phantom upscale. A free-text client comment saying “clean and empty” could hijack the action the same way.

FIX Read the explicit action and room fields first. Fall back to scanning only after stripping the job ID, the comments, and internal keys.

DECISION

Reserve the quota before generating, not after

The obvious design puts the ledger write on a parallel branch. It isn't on the critical path, after all. n8n then runs it last, so the “mark done” update finds no row, and every job stays stuck at pending.

TRADE-OFF Reserving up front means a failed generation still burns a credit. That is the correct trade: it also closes the race where two people spend the last unit at once.

PITFALL

The before/after slider that never moved

The handle dragged, the CSS variable updated, and the image stayed frozen at fifty percent. The cause was one line added weeks earlier: @property --pos { inherits: false }. The variable changed on the parent; the children reading it no longer inherited it.

Every test I had written checked the variable. None checked the rendered pixel.

FIX inherits: true, then a rewrite onto a native <input type="range">. The lesson outlived the bug: assert on what is painted, not on what you set.

Results

Running in production. The staging workflow is the agency's most-used internal tool, and the same pipeline was later repackaged as a product that any agency can self-host, rebuilt generically, with no trace of the original.

Still open. Image quality is judged by a human, not a test. There is no automated check that a render kept the room's geometry; the fallback is the regenerate button.

Stack

  • n8n, self-hosted: form, orchestration, quota, cost dashboard
  • Gemini image model: furnish, declutter, empty, upscale
  • Google Drive (shared drive): renders survive the person who made them
  • n8n Data Tables: the job ledger
  • Vanilla JS front end, served straight from a webhook

What I took away

Image models weight the end of the prompt. Whatever you care about most goes last, after the client's free-text wishes, not before them.

And the bug that cost me the most hours was not in the AI at all. It was a CSS inheritance flag, hidden behind tests that asserted on the wrong thing.