ALANAYALA
Back to Blog

April 2026

If You're Terrible at Creating Outfits, I Built an AI System That Does It For You

Also published on Medium ↗

Between navigating a global business college rotation that has me living out of a suitcase from the UAE to India to Mexico, and balancing early-stage startups, decision fatigue is my daily reality. At 7:00 AM, my brain doesn’t want to compute whether a linen overshirt works with navy chinos, or if I’m packing correctly for a 10-kilo cabin bag limit to Singapore.

I needed an AI stylist. But I absolutely refused to download another standalone app that forced me to manually log everything I wore. I already organize my life in Notion, so my stylist needed to live there, too.

What started as a simple weekend webhook script has evolved into an asynchronous, event-driven pipeline featuring multi-tiered caching, cascading LLM fallbacks, and strict JSON schemas.

Here is a look at how I use this system every day, and the architectural deep dive into how I built it.

The Demo: How It Actually Works

The core design philosophy of this project was Notion as a Headless UI. I interact strictly with Notion; the Python backend does all the heavy lifting invisibly.

Scenario 1: The Daily Outfit

It’s Tuesday morning. I open my Notion “Outfit Log.” I create a new daily entry and fill out two fields:

  1. Desired Aesthetic (Multi-select: e.g., Minimalist, Smart Casual)
  2. Prompt (Text: e.g., “Working from a coffee shop, it might rain later.”)

I check the Generate box.

Within seconds, the row updates. The status changes to “Complete,” and the page populates with three exact items from my wardrobe (Top, Bottom, Footwear) and a brief reasoning from the AI explaining why it chose them based on today’s local weather forecast.

Scenario 2: The Travel Packing Engine

Packing is notoriously stressful. Now, when I have a trip, I go to my “Travel Planner” database. I input my destinations (e.g., New Delhi, Singapore), my dates, and tag my luggage constraints (e.g., Cabin Bag: 10kg).

I hit “Generate Travel Packing.”

The AI looks at the historical weather for those destinations, calculates the trip duration, and generates a cohesive packing list. Crucially, it doesn’t just give me a text list ,  it actually goes into my main Wardrobe Database and checks a “Trip-Worthy” boolean box next to every specific shirt, pant, and shoe I need to pack.

[Insert Screenshot: Your main Wardrobe Database filtered by "Trip-Worthy = True", showing exactly what goes in the suitcase.]

Scenario 3: The “Hamper” Workflow (State Management)

An outfit generator is useless if it recommends clothes that are currently sitting in the laundry.

When I take off my clothes at the end of the day, I click a “Send to Hamper” checkbox on that day’s outfit log. A background pipeline triggers, finds those specific items in my database, unchecks their “Clean” status, and moves them to the “Dirty Clothes” database.

The Architecture: Bridging the UI and the AI

Making this feel like magic on the frontend required navigating some serious architectural landmines on the backend. Here is how the system actually processes these real-world scenarios.

1. Keeping the UI Snappy (Escaping the Synchronous Trap)

When I check that “Generate” box, Notion fires a webhook to my server. Initially, I built this in Flask. Notion would fire the webhook, Flask would query the wardrobe, call the LLM, and update Notion.

The Problem: Notion webhooks expect a response quickly. If the LLM took 15 seconds to think, Notion assumed the webhook failed and would retry, causing my server to generate the outfit three times.

The Fix: I ripped out Flask and rebuilt the entry point using FastAPI to leverage Python’s native asyncio event loop. Now, when Notion fires a webhook, FastAPI instantly returns a 200 OK status, satisfying Notion, while pushing the actual heavy lifting to a background task.

Python

# services/webhook_server.py
@app.post("/webhook/notion")
async def handle_unified_notion_webhook(request: Request, background_tasks: BackgroundTasks):
# ... Webhook deduplication and validation ...

if workflow_type == "outfit":
# Fire and forget. Keep the Notion UI snappy.
background_tasks.add_task(handle_outfit_workflow, page_id)
return {"message": "Outfit workflow triggered", "workflow": "outfit"}

2. Taming AI Hallucinations (Deterministic JSON Schemas)

In my first iteration, I fed the AI a list of my clothes and asked it to return an outfit. I then used regex to map the AI’s text back to my database.

The Problem: LLMs are probabilistic. Sometimes it would output “Black Chinos”, sometimes “Chinos (Black)”, and sometimes it would invent a “Blue Denim Jacket” that I didn’t even own. The pipeline constantly broke.

The Fix: To turn a conversational AI into a reliable microservice, you have to strip away its freedom. I updated the pipeline to use Gemini 2.5 Flash as the primary agent, enforcing response_mime_type="application/json".

Instead of asking for names, I feed the LLM my wardrobe items mapped to their unique database IDs, and provide a strict schema it must adhere to. No regex, no fuzzy matching. Just perfect, 1:1 deterministic mapping.

Python

# core/llm_agents.py
prompt = f"""
You MUST return a raw JSON object string of the following schema:
{{
"selected_ids": [
"exact-id-from-tops",
"exact-id-from-bottoms",
"exact-id-from-footwear"
],
"reasoning": "Brief explanation of choices."
}}
"""

3. The Cascading Fallback Pattern

What happens if the Gemini API goes down while I’m trying to pack for a flight? Redundancy.

My OutfitLLMAgents class implements a cascading fallback pattern. If Gemini times out or fails to return valid JSON, the system gracefully catches the error and cascades down to Groq (Llama 3) using a similar JSON-enforced schema. If Groq fails, it falls back to a hardcoded rule-based Python logic engine. The system always returns an outfit.

4. Speed vs. Freshness (The Data Hierarchy)

Querying my entire wardrobe database directly from Notion for every single LLM call was taking too long. Notion’s API is great, but it’s not built for high-frequency, low-latency reads.

To solve this, I instituted a hierarchical data manager:

  • Memory / Redis: The fastest layer for webhook deduplication.
  • Supabase (PostgreSQL): The primary read-replica of my wardrobe.
  • Notion: The source of truth, but the last resort for data fetching.

I run a background sync that updates Supabase whenever my Notion wardrobe changes (like the Hamper workflow). When the orchestrator wakes up to build an outfit, it pulls my clean clothes from Supabase in milliseconds, ensuring the LLM context window is populated instantly.

How to Build It Yourself (Deployment Recipe)

If you want to orchestrate a system like this, here is the deployment reality. Because of the ASGI architecture and background tasks, deploying this to a platform like Render is highly recommended.

  • The Server: Deploy the FastAPI app (render_server.py using uvicorn).
  • The Cache: Spin up a free Redis instance (via Render or Upstash). This is critical for webhook deduplication ,  otherwise, you will get caught in loops.
  • The Database: Mirror your Notion databases in Supabase. Your Python scripts should read from Supabase (for speed) but write to Notion (for the UI).
  • The Automations: In Notion, set up your Database Automations to fire a Webhook to your Render URL whenever your specific trigger properties (like the “Generate” checkbox) are edited.

he Philosophy of Personal Systems: Function Over Friction

The real power of this system isn’t in a flashy interface or a complex onboarding flow ,  it’s in its invisibility. Most “smart” tools fail because they demand a change in human behavior. They ask you to leave the apps where you already live and breathe to manage yet another silo of data.

I didn’t build this to be a “Product” first; I built it to be a system that adapts to my usage. In a world obsessed with pixel-perfect UI, I prioritized a “Headless” philosophy. My UI is a single Notion checkbox. My feedback loop is a row update in a database I already use to manage my startups and college rotations. By making the backend asynchronous and deterministic, I ensured that the technology serves my morning routine, rather than my morning routine serving the technology.

From Personal Edge to Product-Market Fit?

Could this become a standalone product? Perhaps. If there is enough Product-Market Fit (PMF) for people who want high-utility automation without the overhead of a dedicated app, there’s a path forward. But for now, its value lies in being tailor-made. It’s an example of how LLMs, when treated as fragile software components rather than magic chatboxes, can be engineered into reliable, daily-use infrastructure.

Open Source and Open to Improvement

This project is now entirely Open Source. I built this to solve my own decision fatigue between flights from New Delhi to Singapore, but the architecture, the FastAPI entry point, the Redis caching layer, and the cascading LLM fallbacks, is a blueprint that anyone can adapt.

Whether you want to build your own AI stylist or apply this event-driven pipeline to a completely different problem, the code is yours to hack, improve, and break.

Feel free to fork the repo, submit a PR, or just use it to finally figure out if those navy chinos work with that linen overshirt.

🚀 GitHub Repository: Wardrobe Assistant

Now, if you’ll excuse me, my AI is telling me to pack a linen blend for my flight to Singapore.