Browse case studies by tool and use case: automations, AI agents, dashboards, and custom platforms.
This is the complete text of the site: a contents list naming every section and case study, then all 83 case studies in full, ordered from shortest to longest. Each section also has its own page at /full/<section>.
If your tool shows only the first part of this page: the page is about 296,000 characters and some tools read only the first 100,000 at a time. The same content is also split into 4 parts that each fit in one read. What you have read so far matches Part 1; continue with the next part:
Created Google Apps Script to automatically format data into PDF invoices and send them via email, saving manual effort and enhancing operational efficiency.
Section: Google Apps Script
Created Google Apps Script to automatically format data into PDF invoices and send them via email, saving manual effort and enhancing operational efficiency.
This scenario is implemented so that whenever the status of an opportunity is changed in Go High Level, the associated email is added to Beehiv as a subscriber.
Section: Make Automations
This scenario is implemented so that whenever the status of an opportunity is changed in Go High Level, the associated email is added to Beehiv as a subscriber.
The automation is designed to send daily updates to Slack. The data in Google Sheets is updated daily, and the KPI data is sent to Slack every day using a Make scenario.
Section: Make Automations
The automation is designed to send daily updates to Slack. The data in Google Sheets is updated daily, and the KPI data is sent to Slack every day using a Make scenario.
Created a script to automate data entry in an inventory sheet from CSV files.
Section: Google Apps Script
Created a script to automate data entry in an inventory sheet from CSV files. The script also includes a data cleaning function to ensure accuracy and integrity of the imported data.
Image: Multi-warehouse inventory data with flagged data-quality issues the cleaning step catches: mismatched locations and unidentified SKUs.
Implemented a robust data pipeline using SyncWith and SQL to extract and sync data from BigQuery to Google Sheets for real-time analytics and reporting.
Section: Google Apps Script
Implemented a robust data pipeline using SyncWith and SQL to extract and sync data from BigQuery to Google Sheets for real-time analytics and reporting.
Image: A scheduled BigQuery SQL query joining employer and sign-up data by month for the KPI sheet.
Created an interactive dashboard within Google Sheets for effective data visualization and tracking, serving as a quick reference tool for stakeholders.
Section: Google Apps Script
Created an interactive dashboard within Google Sheets for effective data visualization and tracking, serving as a quick reference tool for stakeholders.
Image: A CRM dashboard inside Google Sheets tracking hiring totals, job closures, and feedback scores.
Worked on developing a Project Tracker in Google Sheets to allocate tasks, track project milestones, and monitor team performance, complete with real-time updates and visual dashboards for easy interpretation.
Section: Google Apps Script
Worked on developing a Project Tracker in Google Sheets to allocate tasks, track project milestones, and monitor team performance, complete with real-time updates and visual dashboards for easy interpretation.
Image: A Gantt-style project schedule generated directly in Google Sheets.
This Zap connects Skool, Slack, LeadConnector and Close, starting when a member answers the Skool membership questions.
Section: Zapier Automations
This Zap connects Skool, Slack, LeadConnector and Close, starting when a member answers the Skool membership questions. It then sends a message to a Slack channel, adds or updates the contact in LeadConnector, and looks up the matching contact and lead in Close.
Image: The five-step Zap: a Skool trigger, a Slack channel message, a LeadConnector contact update and two Close lookups.
This Zap streamlines the process of appointment cancellations by integrating ClickUp with Calendly.
Section: Zapier Automations
This Zap streamlines the process of appointment cancellations by integrating ClickUp with Calendly. When a cancellation is initiated from within ClickUp, by pressing the cancel button on a specific task, the relevant appointment is automatically cancelled in Calendly. This automation ensures that appointment slots are efficiently managed and updated in real time, reducing manual oversight and the potential for scheduling conflicts.
Developed an app script to automatically update columns in an inventory sheet based on values in another sheet, enabling seamless data synchronization within Google Sheets.
Section: Google Apps Script
Developed an app script to automatically update columns in an inventory sheet based on values in another sheet, enabling seamless data synchronization within Google Sheets.
Image: Syncing active/inactive status between a main sheet and a linked inventory spreadsheet.
Image: A custom menu for updating inventory values and normalizing leading zeros on demand.
This Zap automates the process of task creation in ClickUp based on incoming emails from a specified sender in Gmail.
Section: Zapier Automations
This Zap automates the process of task creation in ClickUp based on incoming emails from a specified sender in Gmail. Whenever an email is received from the designated sender, the Zap automatically creates a new task in ClickUp. Additionally, any attachments included in the email are uploaded and attached to the newly created ClickUp task, ensuring all pertinent information and documents are consolidated and accessible directly within the task.
A Looker Studio dashboard tracking Google Ads cost, revenue, orders, profit and ROAS for the same multi-channel account.
Section: Looker Studio
Paid Media Reporting
The same multi-channel Looker Studio report includes a dedicated Google Ads page, tracking cost, revenue, orders, profit and ROAS alongside clicks and conversion rate.
Image: Google Ads dashboard for the same account: cost, revenue, orders, profit and ROAS alongside clicks and conversion rate.
The dashboard runs on Looker Studio so paid media and finance read from the same live numbers instead of a separate export from Google Ads.
A Google Sheets weekly tracker that brings Google Analytics traffic and Klaviyo email and SMS performance into one view.
Section: Dashboards & Reporting
Marketing Reporting
A weekly tracker pulls Google Analytics traffic and Klaviyo email and SMS performance into one Google Sheet, so acquisition and retention numbers sit side by side instead of living in separate tools.
Image: Weekly dashboard tracking Google Analytics traffic by source alongside Klaviyo email and SMS sales and subscriber counts.
Built in Google Sheets so marketing can compare traffic sources, email and SMS sales and subscriber growth week over week without logging into each platform.
Shopify to Google Sheets Data Transfer with Task Allocation
Leveraged expertise in API integration and Google Sheets to create an automated system that transfers data from Shopify to Google Sheets using the ‘Sync With’ extension.
Section: Google Apps Script
Leveraged expertise in API integration and Google Sheets to create an automated system that transfers data from Shopify to Google Sheets using the ‘Sync With’ extension. Utilized Google Apps Script and various functions to allocate customer-specific tasks to agents, and integrated this data into Aircall for seamless customer outreach. The system is set to update daily.
Image: The task-distribution function that splits unattended calls evenly across the agent roster.
A Looker Studio dashboard rolling up Facebook ad spend, revenue, ROAS and profit margin across a 9-month window.
Section: Looker Studio
Paid Media Reporting
Part of a larger multi-channel Looker Studio report, this page rolls up spend, revenue and profit for Facebook campaigns specifically, alongside a daily revenue-vs-spend trend.
$3.94M: Facebook spend (9 mo.)
$3.99M: Facebook revenue
1.01: Facebook ROAS
27,184: Orders
Image: Facebook performance overview across a 9-month window: spend, revenue, ROAS and profit margin alongside a daily revenue-vs-spend trend.
The dashboard runs on Looker Studio so paid media and finance read from the same live numbers instead of a separate export from Meta.
This Zap facilitates seamless integration between Google Calendar and ClickUp, enhancing task management and operational efficiency.
Section: Zapier Automations
This Zap facilitates seamless integration between Google Calendar and ClickUp, enhancing task management and operational efficiency. Whenever a new event is created in Google Calendar, the Zap automatically generates a corresponding task in ClickUp. It transfers relevant details from the calendar event, such as the date, time, and event description, to the ClickUp task. Furthermore, the Zap is configured to assign these tasks to specific individuals, optimizing task allocation and ensuring the right team members are engaged with relevant events and responsibilities.
Automatically collect detailed info from YouTube videos of popular creators for content analysis.
Section: AI Agents
Objective:
Automatically collect detailed info from YouTube videos of popular creators for content analysis.
Implementation:
The workflow pulls the url from your database, scrapes their details using a scraping tool, waits for results, and then saves everything back to the database.
Outcome:
Triggered by a Click: You hit “Start” to run the process.
Grabs Video Links: It picks up YouTube links marked as “ready” in your Airtable.
Runs the Scraper: Sends each link to an automation tool that fetches all available video details (title, channel name, views, likes, subtitles, description, and thumbnail).
Waits for Results: Gives the scraper time to finish collecting the info.
Saves Everything Back: Once done, all the data is pushed back into Airtable, organized and ready for your analysis.
TikTok Video Downloader without watermark: Save Any Reel Directly to Google Drive
To automatically download a TikTok video and store it in Google Drive with a public share link.
Section: AI Agents
Objective:
To automatically download a TikTok video and store it in Google Drive with a public share link.
Implementation:
This workflow fetches a TikTok video, extracts its direct video link, downloads it without watermark, and saves the file to your Google Drive, for content reposting.
Outcome:
One Click to Start: The flow begins with a simple tap to kick things off.
Fetches the Page: It opens the TikTok video page and grabs all the HTML data.
Extracts the Video URL: A smart code block digs into that HTML and pulls out the real video link.
Downloads the Video: The video file is then downloaded using that clean URL.
Uploads to Google Drive: The downloaded video is saved directly to your Drive.
Creates Shareable Link: The file is made public so you can share it with just one link.
TidyCal Booking to LeadConnector and Google Sheets Zap
This Zap runs when a new booking is made in TidyCal, adds or updates the contact in LeadConnector, and records the booking in Google Sheets.
Section: Zapier Automations
This Zap runs when a new booking is made in TidyCal, adds or updates the contact in LeadConnector, and records the booking in Google Sheets. After a filter and a text formatting step, it looks up the matching row in the spreadsheet and then splits into two paths, so each booking ends up in the sheet.
How it works
Starts on a new booking: the TidyCal “New Booking” trigger starts the Zap.
Filters and formats: a Filter by Zapier step checks conditions and a Formatter step works on text.
Updates the CRM: adds or updates the contact in LeadConnector.
Looks up the sheet: finds the matching row in Google Sheets.
Splits into two paths: one updates the spreadsheet row and the other creates a new row.
Image: The ten-step Zap: a TidyCal booking trigger, filter and text formatting, LeadConnector, a Google Sheets lookup, and two paths that update or create a row.
AI-Powered Social Post Generator: From Spreadsheet to Socials in Seconds!
Automatically create short, engaging, and fact-based posts (with emojis!) based on your content ideas, and publish them to the right platform.
Section: AI Agents
Objective:
Automatically create short, engaging, and fact-based posts (with emojis!) based on your content ideas, and publish them to the right platform.
Implementation:
This workflow grabs an idea from your Google Sheet, uses AI to turn it into a social-ready post, and then posts it to either LinkedIn or X (Twitter), depending on your selection.
Outcome:
Kickstarts with a Click: You run the workflow manually to process new ideas.
Reads the Sheet: It pulls content ideas and target platforms from your Google Sheet.
Checks Status: Only picks ideas that haven’t been published yet (marked as 0).
Uses AI to Write the Post: Google Gemini crafts a short, fact-based, emoji-filled post tailored to the selected platform.
Posts It Where It Belongs
If the platform is LinkedIn, it publishes there.
If it’s X (Twitter), it posts accordingly.
Done. One idea, one click, and you’ve got content live on the right platform, no copy-pasting needed.
Daily Ticket-Count Automation: Gmail PDFs to BigQuery and Google Sheets
A Python job that reads daily Ticketmaster audit PDFs from email, stores every figure in BigQuery and publishes a Google Sheet that mirrors the team’s existing workbook.
Section: Python Automation
The Problem
An events company received daily Ticketmaster audit PDFs by email and re-typed the figures into a workbook by hand.
The Solution
A daily job reads the report emails through the Gmail API (with an OAuth refresh token), downloads and parses the audit PDFs, stores every figure in BigQuery, and publishes a Google Sheet that mirrors the team’s existing workbook. It was built spec first: a design spec, then an implementation plan, then tests with fixture PDFs, then the pipeline.
Image: The project repository, with the ticket-count source code, tests, scripts and design documents.
Image: The Google Sheet the job publishes: tickets sold per day against capacity, split into GA and VIP, with the percentage sold. Event names and some tab names are hidden.
Image: The Run Log tab, with one row for each run of the job and its time.
Built with
Python, the Gmail API (OAuth), PDF parsing, BigQuery, the Google Sheets API and pytest.
Smart AI Podcast Audio Script Generator from BBC News
To turn positive and interesting news stories into podcast-style audio scripts using AI.
Section: AI Agents
Objective:
To turn positive and interesting news stories into podcast-style audio scripts using AI.
Implementation:
The system collects news stories from the BBC website, checks if they are podcast-friendly, rewrites them into an engaging script, and finally prepares them for audio conversion.
Outcome:
Starts Manually: The process begins with a button click to start the flow.
Fetches the News: It visits the BBC homepage and grabs all the latest news headlines and descriptions.
Filters the Content: It goes through the list and picks out only 10 headlines.
Checks Suitability: Each headline is checked to see if it’s interesting, positive, and fit for a podcast.
Gets Full Articles: For the suitable headlines, it goes to the full news page and pulls in the main content.
Creates the Script: The stories are then rewritten into a warm, friendly, and engaging podcast script.
Checks Script Output: It makes sure the script is properly prepared.
Sends for Audio: If everything looks good, the script is sent to a tool that can convert the text into voice.
Smart Amazon Product Tracker for Price Drop Alerts
To fetch product details from Amazon and alert you if the price drops, because who doesn’t love a good deal? 💸
Section: AI Agents
Objective:
To fetch product details from Amazon and alert you if the price drops, because who doesn’t love a good deal? 💸
Implementation:
The workflow grabs product links from a database, scrapes product info, compares old vs new prices, updates the records, and, if there's a discount, sends you an email so you can grab it before it’s gone.
Outcome:
Kick-off Daily: Schedule to run daily.
Grabs Products from Airtable: It checks your list to see which items are being tracked.
Launches the Scraper: The link is sent to Apify, which fetches the product’s name, price, and more.
Pulls the Results: Once scraping is done, it collects all the fresh details.
Updates the Product Info: It neatly saves everything back into your Airtable list (because organized data is happy data).
Checks for Price Drops: If today’s price is lower than the last one, the flow gets excited 🤑
Sends You an Email Alert: You get an email telling you “Hey, the price just dropped!” with a link to check it out.
Keeps Things Updated: Whether the price dropped or not, it always saves the latest price for next time.
Automated PDF Data Extraction from Gmail and Google Sheets Integration
Developed an automated solution for a client who received daily PDFs via email.
Section: Google Apps Script
Developed an automated solution for a client who received daily PDFs via email. Created a Google Apps Script that scans Gmail for specific emails containing PDF attachments, downloads and parses these PDFs to extract vital information, and transfers this extracted data into a designated Google Sheet. The script is scheduled to run automatically at a pre-defined time each day, ensuring timely data processing and minimal manual intervention.
Features
User Authentication: Secure login mechanism to authenticate users.
PDF Upload: Interface to upload PDF files manually.
Data Extraction: Extract specific fields like order code, shipping address, product details, etc., from the uploaded PDF.
Google Sheets Integration: Populate the extracted data into a Google Sheet.
Scheduled Email Fetching: Automated function to fetch PDF attachments from a user’s email at scheduled intervals.
Data Validation: Validation checks to ensure data consistency and accuracy.
Image: Feature list and the two-workflow design: a scheduled main flow (PDF parsing → validation → sheet integration) alongside a manual upload flow (login → upload).
Image: The FastAPI backend that handles the upload, invoice parsing, and Sheets integration.
Trello Cards to AI Social Captions and Hashtags Zap
This Zap runs every day, goes through the cards on a Trello board, and uses ChatGPT to write a social media caption and hashtags that are saved on each Trello card.
Section: Zapier Automations
This Zap runs every day, goes through the cards on a Trello board, and uses ChatGPT to write a social media caption and hashtags that are saved on each Trello card. It looks up the cards from a single Trello pillar and loops over them, so every step after the loop runs once for each card. For each card it checks the card, reads its attachments, asks ChatGPT for the caption and hashtags, and updates the Trello card with them.
How it works
Runs every day: a Schedule by Zapier trigger starts the Zap once a day.
Gets the cards: gets the cards from a single Trello pillar, splits them into individual items with the Itemizer and loops over them. Every step below runs once for each card.
Checks each card: a webhook GET request and a Formatter utility step run, and a filter then decides which cards continue.
Reads the attachments: finds the card’s attachments in Trello.
Writes with AI: sends the content to ChatGPT (OpenAI) in a conversation step to write the caption and hashtags.
Saves the result: updates the Trello card with the caption and hashtags.
Image: The first ten steps of the Zap: a daily schedule, the Trello card lookup, a loop over each card, a filter, the card attachments, ChatGPT and a Trello card update.
Trello to Facebook, LinkedIn and Instagram Social Posting Zap
This Zap runs every day, picks up the Trello cards for that day, and publishes their images and captions to Facebook, LinkedIn and Instagram.
Section: Zapier Automations
This Zap runs every day, picks up the Trello cards for that day, and publishes their images and captions to Facebook, LinkedIn and Instagram. Each card holds the post images and a caption for each platform, so content is prepared in Trello and published without manual uploading.
How it works
Runs every day: a Schedule by Zapier trigger starts the Zap once a day.
Finds the cards: gets the cards from a single Trello pillar, splits them into individual items and loops over each one.
Checks each card: Formatter steps convert the start and due dates and read the card status, and a filter then decides which cards continue.
Gets the content: extracts the social captions, then finds the card’s image attachments in Trello.
Updates Trello: updates the card and moves it to another list.
Publishes everywhere: Code by Zapier and Paths split the card into branches by platform and content type, then publish posts and videos to Facebook Pages, company updates to LinkedIn, and photos and videos to Instagram for Business.
Image: Steps 1–11: the daily schedule, the Trello card lookup, a loop over each card, and date and status formatting.
Image: Steps 12–48: caption extraction, Trello updates, and the Paths that publish to Facebook Pages, LinkedIn and Instagram for Business.
AI-Driven Instagram Video Breakdown: Decode What Makes Viral Content Tick
Automatically fetch and analyze top-performing Instagram videos from popular creators to uncover what makes them stand out.
Section: AI Agents
Objective:
Automatically fetch and analyze top-performing Instagram videos from popular creators to uncover what makes them stand out.
Implementation:
The workflow starts by pulling Instagram usernames from your list, finds the creator’s most viewed video, and uses AI to break it down (frame by frame, word by word) to reveal the hidden ingredients of viral success.
Outcome
Schedule it to run: .Schedule it to run daily, weekly or monthly.
Fetches Creators: It pulls Instagram usernames from your Airtable.
Finds the Top Video: From each creator, it picks the reel with the highest views, because we only care about what’s working 📈
Stores the Essentials: Saves the video link, caption, creator name, and view count.
Downloads the Video: Preps the reel so we can take a closer look under the hood.
Uses AI to Analyze the Magic: Here’s where it gets spicy 🔍
What’s the hook? How does it grab attention in the first 3 seconds?
What’s the vibe? What’s happening in the video: background, setting, energy?
Visual style: What kind of text, clothes, or framing makes it pop?
Game-changer moments: What moment makes viewers stop scrolling and watch till the end?
Saves the Analysis: The insights are written down and saved back in your Airtable. Boom: structured, searchable, gold-standard content breakdown.
Monday.com Timeline Stage Alerts to Slack and Close Zap
This Zap watches the timeline stages of Monday.com items and notifies the team in Slack each time an item reaches a new stage.
Section: Zapier Automations
This Zap watches the timeline stages of Monday.com items and notifies the team in Slack each time an item reaches a new stage. Items on the Monday.com boards follow a programme timeline: 45, 60 and 90 days in, and 60, 45 and 1 day left. When an item reaches one of these stages, the Zap triggers, updates the item and sends a message to the team’s Slack channel.
At the 45-days-left stage it also checks Close for the opportunity: if it is found, the stage is updated, and if not, a new opportunity is created. A Slack notification goes out either way.
How it works
Starts on a webhook: a Webhooks by Zapier “Catch Hook” step starts the Zap when an item reaches a stage.
Finds the item: looks the item up in monday.com.
Works out the stage: a Code by Zapier step runs Python, then Paths split the Zap into branches for the different timeline stages.
Updates monday.com: each branch updates the item.
Notifies the team: each branch ends with a Slack channel message.
Handles Close: one branch looks up contacts in Close and creates a lead and an opportunity where needed, before notifying Slack.
Image: Steps 1–16 and 31–33: the webhook trigger, the monday.com lookup, the Python step, and the Paths that update items and notify Slack.
Image: Steps 17–30: the Close branches that find contacts and create leads and opportunities, each ending in a Slack message.
Automation of Shopify Orders to Google Sheets for Dropshipping E-commerce Store
The primary goal of this automation is to streamline the process of managing Shopify orders by automatically distributing them to the appropriate fulfillment sheets.
Section: Make Automations
The primary goal of this automation is to streamline the process of managing Shopify orders by automatically distributing them to the appropriate fulfillment sheets. This is particularly useful for a dropshipping e-commerce store that sources products from multiple suppliers, such as AliExpress.
Order Creation: Whenever a new order is created in Shopify, the automation system adds it to a data store built within Make, making sure all orders are logged and tracked efficiently.
Product Identification: The automation includes a scenario that checks the product IDs in a Google Sheets database. By matching these product IDs, the system determines which specific fulfillment sheet each order should be directed to, based on the supplier.
Master Sheet Integration: A master sheet is maintained where all order data is consolidated. This master sheet serves as the central point for tracking and managing all orders. Whenever any changes are made to the master sheet, the automation ensures that these updates are reflected in the respective Google Sheets, maintaining consistency and accuracy across all fulfillment sheets.
Supplier Coordination: By automating the distribution of orders to the correct fulfillment sheets, the system ensures that each supplier receives the correct order information, facilitating efficient and accurate order fulfillment.
Python MCP Server Connecting Claude to a Field-Service CRM
A hosted, OAuth-protected MCP server in Python that lets a home-services business ask Claude about customers, jobs, invoices and leads, with write actions hidden unless allowed.
Section: Python Automation
The Problem
A home-services business wanted to ask Claude questions about customers, jobs, invoices and leads, and to take safe actions, without giving it raw database access.
The Solution
A Model Context Protocol server that exposes the full REST API of the business’s field-service software (Housecall Pro) as about 20 domain modules: customers, jobs, estimates, invoices, leads, employees, scheduling, webhooks and more. They are all mounted under one FastMCP app. It is deployed on Railway and connects straight into claude.ai.
Notable details
OAuth login sits in front of the server (GitHub provider, redirecting back to claude.ai), so no shared API keys are handed to users.
A read-only enforcer middleware hides every tool whose name contains create, update, delete, send, void or dispatch from the tool list, unless it is explicitly allowed. This is per-agent permission control at the protocol level.
Tools are namespaced (customers_*, jobs_*, invoices_*), so the model cannot confuse one domain with another.
Image: The server entry point: GitHub OAuth in front, one FastMCP app, and each domain module (customers, jobs, estimates, invoices, leads, employees) mounted under its own namespace.
Image: The repository: one Python module for each area of the field-service API, plus a Procfile for deployment on Railway. The commit messages are blurred.
Automated P&L & Sales Tracker for Ecommerce (Google Sheets)
A single Google Sheets template that turns raw sales, COGS and marketing cost data into live real-vs-estimated profit numbers for any date range.
Section: Dashboards & Reporting
Finance & Sales Reporting
An automated profit & loss tracker built in Google Sheets for an ecommerce business. Pulling sales, real COGS, marketing spend and other expenses into one model, it calculates real vs. estimated profit automatically for any date range instead of being rebuilt by hand every month.
$1.18M: Total sales (period)
$54.6K: Total real profit
4.61%: Real profit margin
$587K: Total marketing cost
Image: Monthly sales and profit dashboard: total sales, real/estimated profit, margins and marketing cost calculated automatically from raw order data.
Image: The same template applied to a different brand: totals, COGS and marketing cost recalculated instantly for any date range.
Daily and 30-day views
A daily dashboard view surfaces total and trailing-30-day sales, profit and expense metrics side by side, so the team can catch a bad week before it becomes a bad month.
Image: Daily dashboard: total and last-30-days sales, profit and expense metrics side by side for fast day-to-day monitoring.
Rolling breakdowns
A rolling 12-month view breaks total profit down into COGS, marketing cost, other expenses and disputes won, so the team can see exactly where margin is being gained or lost.
Image: Last-12-months pie chart breaking total profit down into COGS, marketing cost, other expenses and disputes won.
Built as a single Google Sheets template so finance reads live profit numbers for any date range without exporting from five different tools first.
Production Linux Server Audit and Repair for a Data Subscription Site
An audit of the single Ubuntu server behind a financial-data subscription site, a fix for its silently failed social-posting pipeline, and a documented setup so it can be maintained safely.
Section: Python Automation
The Problem
A financial-data subscription site ran a Python backend, scheduled jobs, blog and email automation and a social-posting pipeline, all on one Ubuntu VPS. The social pipeline had silently died. The client needed an audit, a fix and a roadmap.
What Was Done
Audited the whole server (systemd units, cron, logs, nginx, gunicorn, the SQLite database and the backup job) and wrote a plain-English client report with a health score and prioritised fixes.
Fixed the dead social pipeline with two targeted changes and stabilised the scheduling.
Set up maintenance through a git mirror of /opt with a CLAUDE.md that describes the architecture, so Claude Code can work on it safely.
How the system runs
A single source of truth: one SQLite database plus JSON outputs.
20:00 ET systemd timer → nightly_pipeline.sh
ingest OHLC → run scanners → build JSON → DB backup to Drive (rclone)
→ build newsletter + blog post (publish via WP REST API)
→ send daily email (Brevo)
15:45 ET systemd timer → live_pipeline.py (real-time signals + options pricing)
always systemd service → gunicorn Flask API :5500 behind nginx + Cloudflare Tunnel
cron 08:15 / 08:45 / every 15 min market hours / 21:00 → content engine renders PNG/MP4 cards and posts to IG / X / TikTok
Built with
Python, Flask, SQLite, systemd, cron, bash, nginx, gunicorn, Cloudflare Tunnel, rclone, JWT auth, Stripe, the WordPress REST API, Brevo and the Post for Me API.
From a Signed DocuSign Contract to a Master Sheet Row and an Itinerary Slide
Turning a signed DocuSign travel contract into a master sheet row, a filed contract and an itinerary slide, automatically.
Section: Make Automations
Overview
An internal operations automation for a group travel company that manages college formal events.
When a group signs a travel contract in DocuSign, the operations team has several follow-up jobs: record the trip in the master ops spreadsheet, file the signed contract in the group’s Drive folder, and produce a Friday/Saturday/Sunday itinerary slide for the group.
This scenario does those three jobs automatically as soon as a DocuSign envelope is completed.
Built with Make, DocuSign, Google Apps Script, Anthropic Claude (claude-sonnet-4-6), Google Sheets, Google Slides and Google Drive.
The Problem
Each signed contract holds the details the team needs in several places: hotel names and room counts, check-in ages, buses, venues, event times, food and bar packages, and payment and task due dates. Moving these by hand into the master sheet, then building a customer-facing itinerary from them, is repetitive and easy to get wrong.
The Solution
A Make scenario that:
Triggers when a DocuSign envelope is completed.
Downloads the signed document.
Sends it to a Google Apps Script web app, which returns the contract’s fields as structured data.
Asks Claude to write the three-day itinerary text from those fields.
Routes the result down three paths: a new-contract path, an addendum path, and an archive path.
Image: Part of the Make scenario: the DocuSign trigger and download, the HTTP call to the Apps Script web app, Claude, and a router that splits into Google Sheets, Slides and Drive paths.
Image: An example three-day itinerary slide (group, dates, hotel and company details blurred for privacy).
A Slack Bot That Answers Trip Questions from an Ops Master Sheet
A Make scenario that lets a team member ask a plain-English trip question in Slack and get an answer drawn only from the ops master sheet.
Section: Make Automations
Overview
An internal operations tool for a group travel team.
The operations team keeps all trip data in one Google Sheet (a “MASTER” tab in the workbook “Master Ops Fall 26/Spring27”). Each row is one group trip, with columns for dates, destination, ops coordinator, sales rep, contracts, hotel and rooming, transport, events, task due dates, and payment status.
This scenario lets a team member ask a plain-English question in Slack and get a structured answer drawn only from that sheet, without opening it and searching for the right row and column.
Built with Make, Anthropic Claude (claude-sonnet-4-6), Google Sheets and Slack.
The Problem
The sheet is wide. The blueprint’s column list runs from column A to column BH, with more than 55 labelled fields per trip. Finding a single fact, such as a group’s hotel or room counts, means locating the right row, then scrolling to the right column.
The Solution
A six-module Make scenario, triggered by a webhook, that:
Receives the Slack question and channel.
Uses Claude to turn the question into structured search terms.
Searches the master sheet for matching trips.
Flattens the matching rows into text.
Uses Claude again to answer from that text only.
Posts the answer back to the same Slack channel.
Image: The Make scenario: a custom webhook, Claude turning the question into search terms, a Google Sheets search, a text aggregator, Claude answering from the results, and a Slack message.
Image: An example answer in Slack: the question, the search terms Claude worked out, and the matching groups with their dates and destination (group names blurred for privacy).
AI-Powered RAG Chatbot for The Fast Bite: Seamless Ordering and Query Handling
Create an AI-powered chatbot that helps The Fast Bite( a fast food restaurant) customers with menu queries, placing orders, and providing restaurant details in a fast and friendly manner.
Section: AI Agents
Objective:
Create an AI-powered chatbot that helps The Fast Bite( a fast food restaurant) customers with menu queries, placing orders, and providing restaurant details in a fast and friendly manner.
Implementation:
The RAG Chatbot for The Fast Bite automatically updates itself with the latest menu and restaurant info from Google Drive and quickly uses that to help customers with orders or questions. It provides fast and accurate responses to make the customer experience smoother.
Outcome:
Triggered by New Files
The chatbot starts working whenever a new file is added to The Fast Bite’s Google Drive folder. This could include updates to the menu or any other important information.
Downloading the Information
The chatbot downloads the new file automatically from Google Drive for processing.
Understanding the Data
It uses Google Gemini (AI) to turn the downloaded content into a format that the chatbot can understand.
Storing the Info
The processed data is stored in a system called Pinecone. This helps the chatbot quickly find the answers when customers ask questions.
Handling Customer Queries
When customers ask questions, the chatbot uses this stored information to provide relevant and helpful responses instantly.
Key Features
Fast Updates: The chatbot is always up-to-date with the latest menu or restaurant information.
AI-Powered Answers: It uses advanced AI to provide accurate, friendly responses.
Seamless Experience: The chatbot helps customers with ordering, customizing their meals, and answering common questions (like store hours, promotions, etc.).
Two n8n workflows that find athlete news, draft the post, and publish it to Instagram once a person approves.
Section: n8n Automations
Purpose
PeakNIL connects college athletes with brands for NIL deals. To stay visible and attract brands, it needs a steady stream of social media posts celebrating athlete achievements. These two workflows automate that process, from finding the news to publishing on Instagram, with a human approving every post.
The Problem
PeakNIL has hundreds of athletes across 28 sports, and someone has to check news about each of them.
Staff must decide which stories are worth posting, write captions, find usable photos, design graphics and publish.
Doing this by hand is slow and expensive, and it can’t keep up with fast-moving sports news.
Posting is inconsistent, so some athletes get covered and others are missed.
Athlete records are often incomplete, which makes tracking harder.
Posts must stay positive and brand-safe, with no injuries, losses or controversies.
The Solution
Two connected automations handle the work, and a person approves the result.
Workflow 1: Athlete Monitor (finds and drafts)
Runs every weekday and picks athletes from the roster sheet. It skips anyone with missing data or scanned in the last 3 days.
Searches for recent news (last 3 days) from trusted sports sources.
Claude decides whether the story is worth posting, assigns a content category, and writes the caption and a short headline.
Finds a clean photo of the athlete and emails the draft to the content manager for review.
Image: Workflow 1, Athlete Monitor: a schedule trigger reads the roster from Google Sheets, searches for news, has Claude draft the post, and emails it to the content manager through Gmail.
Workflow 2: Reply Detection (approves and publishes)
The manager replies to the email with an image number or “approve”.
The workflow reads the reply, builds a branded graphic with the chosen photo and headline, and sends it back for a final check.
Once approved, it publishes the post to Instagram and updates the tracking sheet.
Image: Workflow 2, Reply Detection: a Gmail trigger reads the manager’s reply, then one branch builds the graphic with Placid and another publishes to Instagram after approval.
The Result
Content creation drops from hours of manual work to a quick reply by email.
Posting is consistent across the whole roster.
Only positive, relevant stories go out, and nothing publishes without human approval.
Incomplete athlete records are flagged automatically so the team can fix them.
Automatic Cleanup of Scheduled Posts When a Card Is Unapproved
An n8n workflow that checks every 15 minutes for cards that are no longer approved and removes their still-scheduled posts from Metricool, without touching anything already published or about to go out.
Section: n8n Automations
Purpose
A content team schedules social posts through Metricool from cards in Airtable. Each card stores the Metricool post ID for X, Facebook, LinkedIn and Threads. If a card is later denied, sent back to review, or its scheduling fails, the posts already scheduled for it would still go out. This workflow finds those cards and removes the leftover scheduled posts safely.
The Solution
Every 15 minutes n8n searches Airtable for cards whose status is Pending Review, Denied, Schedule Failed or blank, but that still hold a Metricool post ID.
A code step turns each card into one item per post ID. IDs that were already handled and logged are skipped, so settled posts are not checked again on every run.
For each ID, n8n asks Metricool for the post, one at a time with a short pause between calls. A failed call does not stop the workflow.
A decision step applies the safety rules below and marks each post as delete, keep, clear or retry later.
The Delete? branch sends the delete request to Metricool for posts marked delete. Every other post goes straight to the merge step, which joins both paths.
A code step builds one update per card. It clears the IDs of posts that were deleted or were already gone, adds a timestamped line to the card’s Unschedule Log for each ID handled, and clears the Scheduled Time when a post was deleted, so approving the card again schedules it fresh.
Airtable saves the update. A lookup or delete that failed is left untouched, so the next run tries it again.
Image: Unschedule cleanup in n8n: a 15-minute schedule trigger, an Airtable search, a code step that splits each card into one item per Metricool ID, a lookup of each post, a decision step, a Delete? branch that either calls Metricool’s delete or skips to the merge, and a code step and Airtable update that clear the IDs and write the log.
Safety Rules
A post that is already published is never deleted.
A post due within the next five minutes, or already past due but not marked as published, is left alone, and the log asks someone to check Metricool.
Only a future, unpublished post is deleted. The same rules apply to every network, X included.
A post that Metricool no longer has counts as already gone, so its ID is simply cleared from the card.
Good to Know
The workflow is live and runs on its own schedule.
Every decision is written to the card’s log, so the team can see what was deleted, kept or cleared, and when.
Metricool is called one post at a time, so a long list of cards does not flood it with requests.
AI-Powered Contract Validation, Human Review, Automated Google Drive Filing, Folder Organization, Missing-Term Detection & Document Tracking.
Section: Custom SaaS Platforms
Overview
The Contract Intake Tool is a web app that uses Claude AI to extract key terms from supplier contracts, flags missing information for human review, and automatically files approved contracts into the correct Google Drive folders. It also maintains a record of all filing activities..
Business Problem
Supplier contracts such as hotel, venue, and bus services) Were reviewed and filed manually, making the process time-consuming. Important terms like cancellation clauses, fees, and tax rates could be missed, and contracts sometimes ended up in the wrong Google Drive folders, making them difficult to find later for billing or disputes.
Proposed Solution
The solution is a web app that uses Claude AI to extract required terms from supplier contracts (PDFs), allows users to review missing information, and automatically files approved contracts into the correct Google Drive folders. PostgreSQL maintains a record of each filing, such as who filed it, what was filed, where it was filed, and when
The application is built using the following technologies:
Next.js: Web application framework.
TypeScript: Application development.
Claude AI: Extracts and checks required contract terms.
Google Drive API: Files contracts into the correct folders.
PostgreSQL: Stores filing records (who filed it, what was filed, where it was filed, and when).
JavaScript PDF Library: Extracts text from uploaded PDF contracts.
Key Features
AI-Powered Term Extraction: Extracts required contract terms using Claude AI.
Missing Term Review: Flags missing information (Not Found) for manual review before filing.
Automated Google Drive Filing: Files contracts into the correct folder based on group name, date, and category.
Filing Activity Log: Records who filed the contract, what was filed, where it was stored, and when.
Category-Based Checks: Checks contract terms according to the selected category (Hotel / Venue / Buses / Other).
Key Safeguards
Human Verification: Contracts with missing terms (Not Found) require manual review before filing.
Controlled Folder Creation: Only the Chapter and Category folders are created automatically; Year, Season, and University folders must already exist in Google Drive.
Filing Audit Log: Each filing records who filed the contract, what was filed, where it was stored, and when.
Closing Summary
The Contract Intake Tool simplifies supplier contract processing by combining AI-powered term extraction, human review, and automated Google Drive filing. It helps reduce manual work, identify missing terms, and maintain a clear record of filing activities.
Reports that used to take days across sites now land by 6am, and we act on them the same morning.
The network ran on four disconnected systems: scheduling, the EHR, billing/claims and finance. Leadership could never see patient flow, provider utilisation and revenue-cycle health in the same place, and each site’s monthly report was rebuilt by hand. I consolidated all four into one governed Power BI model behind two role-based dashboards, refreshed automatically every morning, turning days of manual reporting into a screen the team reads with their coffee.
What Was Leaking
Siloed systems: Scheduling, EHR, billing and finance never lined up, so no-shows, utilisation and collections couldn’t be seen together.
Manual per-site reporting: Every site’s pack was rebuilt by hand (weeks behind by the time it landed, and easy to get wrong).
Invisible revenue leakage: Denials, AR aging and slow collections weren’t tracked in one place, so cash slipped through unnoticed.
Dashboard One: Patient Flow & Utilisation
Appointments vs utilisation, department mix, top sites, a day-by-hour utilisation heat map and capacity gauge.
Utilisation heatmap: Spots under-used clinic slots by day and hour at a glance.
Sites & departments: Ranks volume and no-shows together, site by site.
Volume vs utilisation: Pairs appointment growth with the utilisation trend.
Capacity gauge: Tracks attainment against the plan, currently 96%.
Dashboard Two: Revenue Cycle & Claims
Billed vs collection rate, a claims funnel, AR aging buckets and net revenue by payer.
Image: Revenue Cycle & Claims
Claims funnel: Shows where submissions fall out before they’re paid.
AR aging: Flags the £84K stuck in 90+ days for chase-up.
Billed vs collected: Tracks the collection rate month over month.
Payer mix: Splits net revenue across NHS, insurers and self-pay.
How It’s Built
Stage
What We Built
Tools
Sources
4 systems: Scheduling · EHR · Billing · GL
REST / API / file connectors
Clean
Key standardisation
Power Query
Model
Star schema, shared date table, DAX
Power BI model + DAX
Secure
RLS by site, per-site access
RLS roles
Deliver
06:00 refresh via gateway & service
Power BI Service + gateway
The Impact
After six weeks
+5.0pts: Provider utilisation
−2.1pts: No-show rate
−6 days: Days in AR
3 days → 0: Manual reporting
With utilisation and no-shows visible by site and hour, the network reallocated clinic slots and tightened reminders, lifting utilisation five points while trimming no-shows, and replacing days of manual work with an automatic 6am refresh reconciled to the ledger.
Newsletter Generator: From a Google Sheets Row to a Draft Campaign
A Make scenario that turns one row of a Google Sheets newsletter planner into a finished draft campaign in Campaign Monitor, in one of three layouts chosen by the editor.
Section: Make Automations
Purpose
A publishing business sends a regular email newsletter with news articles, sponsored posts, a destination report and a podcast episode. Building each issue by hand in the email tool means copying titles, images, excerpts and links into a template, block by block. This Make scenario lets the editors plan the whole issue in a Google Sheet and creates the campaign automatically.
The Solution
An editor fills in one row of the newsletter sheet. It holds the titles, images, excerpts, links and category labels for three news posts and three sponsored posts, plus the destination report and the podcast episode.
The editor picks a template number (1, 2 or 3) and sets the Create Newsletter cell to “Create”.
On its schedule, the scenario searches the sheet for a row marked Create and takes one row at a time.
A router checks that a row was found and sends it down one of three routes, depending on the template number.
Each route has a code step that builds the full newsletter from the row. The first route builds the three-row layout. The other two build the mixed layout, with two posts side by side and one centred below, each with its own template.
An HTTP step sends the result to the Campaign Monitor API, which creates a draft campaign from the chosen template.
A final Google Sheets step updates the row with “Done”.
Image: The Make scenario: Google Sheets searches for a row marked Create, a router picks one of three templates, a code step builds the newsletter content, an HTTP call creates the campaign from the template, and Google Sheets updates the row. A Skip step on each route handles errors.
What the Code Builds
The newsletter is built section by section: a top banner, the news posts, a banner, the sponsored posts, the destination report, the podcast episode and a second banner.
Each news and sponsored post gets its image, a category label, its title and excerpt, and a button (Read More or Learn More) linked to the post.
The destination report has an image, a badge, the destination name, a description and a Read Full Report button. The podcast block has an image, a date line, the title, a description and a Listen Now button.
Gaps are filled in for the editors. A missing image on a news or sponsored post is replaced by a default image, a missing link points to the main website, and every issue gets a unique campaign name with a timestamp.
The subject line and the header date default to the current month and year, and the sender details and subscriber list are set in the same step.
Good to Know
The editors work only in the sheet. All copy, images and links live there, so nobody has to open Make or edit a template to produce an issue.
The campaign is created as a draft in Campaign Monitor, so the team can check it there before it is sent.
If Campaign Monitor returns an error, the scenario skips that run instead of stopping with an error.
Detailed ClickUp workspace structure for a staffing company’s operations.
Section: ClickUp Setups
Main Folder: Taraki Staffing Operations
Recruitment
Lists
Job Postings: Manages the lifecycle of job openings.
Custom Fields: Job ID, Role Title, Department, Status (Open, Filled, On Hold), Date Posted, Location, Salary Range, Job Description.
Automations: Notify recruitment team when a job is posted; send email alerts when status changes; auto-archive roles once filled; reminder for follow-up if position remains unfilled past a target date.
Candidate Screening: Initial assessments to filter candidates.
Compare payment and rooming records, identify discrepancies, and keep group travel organized with confidence through faster, smarter, and more accurate data reconciliation.
Section: Custom SaaS Platforms
Project Overview
Paid List vs. Rooming List is a web app that compares group travel payment records with room assignments exported from Google Sheets. It identifies fully paid travelers assigned to rooms, flags missing or unpaid records, and highlights similar-looking names that may need staff review.
Business Problem
Group travel teams maintain separate Paid List and Rooming List tabs in Google Sheets. As lists change, they can become inconsistent: someone may pay but not be assigned to a room, appear on a rooming list without being listed as paid, or have a slightly different spelling of their name across the two lists. Finding these discrepancies manually in spreadsheets with 100 or more rows is slow and error-prone.
An internal tool for a group travel company. Each student travel group keeps two separate tabs in Google Sheets, a Paid List (who paid, how much, and their payment status) and a Rooming List (who’s actually assigned to a room). Those two lists can drift apart: someone pays but never gets added to a room, someone’s on the rooming list but never paid, or a name is slightly misspelled between the two. Catching that manually in a 100+ row spreadsheet is slow and error-prone.
Solution
A small web app where a staff member uploads both CSVs (exported straight from the Google Sheet tabs) and gets back three buckets instantly:
Matched: paid in full and on the rooming list. No action needed.
Paid List issues (🔴/🟨): on the Paid List but missing from the Rooming List, or not marked “Paid in Full.”
Rooming List issues (🔴/🟨): on the Rooming List but missing from the Paid List.
Image: The upload screen: a staff member adds the Rooming List and Paid List CSVs exported from the group’s Google Sheet and clicks Compare.
Image: The results summary and the Paid List issues. Red rows are real discrepancies and yellow rows are similar-name warnings. Names, amounts, room and confirmation numbers are blurred for privacy.
Image: The Rooming List issues: people on the rooming list who are not on the paid list. Names and booking numbers are blurred for privacy.
Image: The matched list: people who paid in full and are on the rooming list, with no action needed. Names, amounts and booking numbers are blurred for privacy.
How It Works
Parse both CSVs: the Rooming List always has a header row; the Paid List usually doesn’t, so the parser detects whichever shape it’s in (including an optional leading row-number column, and an “Amount” or “Payment” column).
Flatten each rooming-list row into individual occupants (up to 2 per room).
Match people by exact, case-insensitive name between the two lists.
For anyone with no exact match, check the other list for a close-but-not-exact name (same last name, or a small edit-distance typo/nickname), if found, flag it yellow (“Similar Name Found”) on both sides as a precaution; if truly nowhere on the other list, flag it red as a real discrepancy. A matched person who isn’t “Paid in Full” is also red.
Results render as sortable tables in the browser, each downloadable as its own CSV.
Everything runs stateless: nothing is saved to disk; each upload is compare-and-respond.
Tech Stack
Built in TypeScript running on Node.js (a plain Node HTTP server, no framework), with a plain HTML/CSS/vanilla JS frontend. Deployed on Railway.
Cold Email Copy Generation with Automatic Quality Checks
An AI tool that writes five cold email sequences in spintax format in minutes and checks each one against eight quality rules, rewriting any that fail.
Section: Python Automation
Overview
Cold email copy generation and quality control. Instead of spending hours writing email variations by hand in spintax format and checking each line against banned phrases and formatting rules, teams now get five production-ready email sequences in minutes.
The Problem
Creating cold email variations meant hours of manual work. Writers had to draft email sequences by hand, then apply spintax format line by line, a tedious process where every variation had to be written and formatted separately. Quality control was completely manual: each line had to be checked against banned phrases and banned words, word counts verified, spintax coverage confirmed, and offer rotation rules checked. With no automated checklist, mistakes slipped through before emails went into the sending tool. Creating five email sequences took 5 to 10 hours of work, was error-prone, and depended entirely on someone being available to do it correctly.
What the Automation Does
Image: The team interface: a chat assistant on the left, a button to generate the week’s strategy, and Copy Variations and History at the top.
Step 1: Email sequences are generated
The automation takes the email parameters and requirements, then uses AI to generate five complete cold email sequences in full spintax format, with multiple variations per email step (A/B variants) so testing options are built in.
Step 2: Automatic quality checks run
Every generated copy is automatically checked against eight quality rules: word count, spintax coverage on every line, banned phrases, banned words, offer accuracy, hook uniqueness, offer rotation compliance, and more. If a copy passes all checks, it is flagged as production-ready. If it fails, the system automatically rewrites it (up to three retries) to meet the requirements.
Step 3: Ready-to-send copies are delivered
The team receives five email sequences with a pass or fail verdict on each. Every copy is either production-ready or clearly flagged as needing manual revision, so there are no surprises or quality issues after the copy goes into the sending tool.
Tools and Integrations Used
Custom code automation (AI-powered email generation and quality control)
Anthropic Claude (copywriting and analysis)
Python and FastAPI (backend automation)
TypeScript, React and Next.js (team interface)
The Result
Teams no longer spend 5 to 10 hours writing and quality-checking email sequences by hand. Five production-ready sequences now generate automatically in minutes. Quality control shifted from a manual checklist to an automated pipeline that catches banned phrases, spintax gaps, word count violations and formatting mistakes instantly. Every copy follows the same proven structure with consistent formatting. What used to take hours of manual writing and QA now happens automatically, freeing the team to focus on strategy and testing instead of production work.
Summary
The tool automates the full cycle from email copy generation to quality-checked delivery. Teams provide their email requirements, the automation generates five complete sequences in spintax format, automatic quality checks verify everything, and production-ready emails are delivered in minutes instead of hours. The team stays in control, since they review and approve the output, while the repetitive writing and quality-checking work is handled automatically.
An n8n workflow that turns one product photo into AI-generated model images plus eCommerce and lifestyle campaign shots, saved to Google Drive.
Section: n8n Automations, AI Agents
1. The Problem: Manual Image Creation for Fashion Campaigns
Before automation, the creative team manually handled photo shoots and model image generation, resulting in:
Time-consuming image conceptualization and generation processes
Need for specialist prompt engineering for AI image generation
Fragmented workflows managing product images
Delays and inconsistency in campaign visuals
The goal was to integrate the entire creative photo shoot pipeline into one automated, AI-powered system to rapidly produce consistent, high-quality fashion campaign images.
2. The Goal: Fully Automated Photo Shoot Workflow in One System
By using n8n with AI models and image generation APIs, this workflow:
Allows uploading a product item image via a simple form trigger
Automatically analyzes the product to build a creative prompt
Generates a photo-realistic product model image matching the style
Further generates high-quality eCommerce and lifestyle product prompts referencing the model and product images
Produces final photorealistic campaign images dynamically and uploads them to Google Drive
Streamlines the entire creative direction and image generation process from one system
3. System Architecture (Simplified)
Phase 1: Image Input & Model Creation
Form Trigger initiates with a clothing image upload
Convert uploaded image to base64 for API usage
Build creative prompt with internal logic for garment analysis and ideal model persona
Call AI image generation API (Google Gemini model) to create a unique fashion model images
Save and upload model images to Google Drive
Phase 2: Prompt Generation & Result Image Creation
Use the generated model image and the original clothing image as references
Build multiple detailed image prompts for eCommerce studio shots and lifestyle scenarios
Generate corresponding photorealistic images via AI API calls
Validate generation success and upload final images to Google Drive
4. RealWorld Impact
Here is how the automation changes creative production:
🟦 Zero manual promptwriting
All creative direction and prompt engineering is baked into the workflow.
🟦 Consistent, onbrand visuals
Every product gets a full set of standardized eCommerce shots plus lifestyle scenes.
🟦 Massive time savings
What used to take hours of backandforth between creative, photography, and AI tools now runs in minutes.
🟦 Scalable for any catalog
You can run this for 1 product or 1,000 products with the same quality bar.
🟦 Organized assets
All outputs go straight into the right Google Drive folder, named in a predictable way.
5. Expected Impact
Here are the KPIs this automation improves
Time Savings
Manual creative + prompt time reduced by 70–90%.
A brand manager or designer saves several hours per campaign.
Error & Inconsistency Reduction
No more forgotten angles, missing lifestyle shots, or halfbaked prompts.
Every product gets the same full coverage: front, angle, back, detail, plus lifestyle.
Content Production Speed
Campaignready visuals can be generated the same day products arrive.
Faster gotomarket for new drops and collections.
Asset Organization
100% of generated images land in the correct Google Drive folder.
Standard naming like “AI Model” and “Result #X” keeps sets easy to browse.
Creative ROI
You reuse the same expert logic and creative direction across unlimited products.
More content, more speed, and less dependency on external shoots for every variation.
Newsletter Performance Agent: Daily Scoring of Every Sent Issue
An n8n workflow that finds each newsletter issue after it is sent, removes machine clicks from the numbers, scores it against the account’s own baseline, and posts an AI analysis to the team.
Section: n8n Automations
Purpose
A publisher sends a regular email newsletter through Kit. This n8n workflow scores every sent issue automatically. It pulls the issue’s stats from Kit, corrects the click numbers for machine traffic, compares the result with the account’s own history, asks Claude for an analysis, and shares it with the team.
The Solution
The workflow runs every day at 17:00 South African time, and it can also be started by hand.
It reads the last 40 logged issues from Airtable and the list of broadcasts from Kit.
It picks the newest completed send from the last 36 hours that has not been logged yet. If there is none, it stops with nothing to score.
It pulls that issue’s stats and its link-level clicks from Kit. If the link report is missing, the run carries on and marks the clicks as raw and unverified.
A code step calculates the metrics, as described below.
It reads the active newsletter rules from Airtable, so the analysis does not suggest rules that already exist.
Claude receives the numbers and the rules, and returns its analysis as structured data.
A second code step cleans the text, keeps at most two proposed rules and labels each one “PROPOSAL, not active”.
The result is saved as a new row in the Performance table in Airtable, with the stats, the score, the analysis and the recommendations.
Two things then happen at the same time. A short summary is posted to the team’s Telegram feedback group and the run is logged. Any proposed rules are saved to the Newsletter Rules table as inactive.
Image: The Newsletter Performance Agent in n8n: a daily trigger or a manual run, a look-up of past issues and Kit broadcasts, a step that picks the sent issue, Kit stats and link clicks, the metrics code, the Claude analysis, and an Airtable row. The row then leads to the Telegram summary and run log on one branch, and to the proposed rules on the other.
What the Code Calculates
Machine clicks are removed. Mail security gateways often click every link in a message once. This shows up as a flat plateau across many links, while real readers produce a steep curve. The workflow detects the plateau and subtracts it, which gives an adjusted click rate. Against past sends, raw click rates of 7.73 and 6.64 percent came out at 2.63 and 2.54 percent, and a clean issue was left untouched.
Each issue is scored against a baseline. The baseline is the median open rate and click rate of past issues. Segment sends, with fewer than half the usual recipients, are left out. A score of 1.00 means the issue was on the baseline.
Clicks are attributed to products and positions. They are grouped by the campaign tag in each link, with the date suffix removed so that a trend can form over time, and by the slot tag that marks where the link sat in the issue.
Anything doubtful is flagged. The flags cover missing link data, segment sends and links without a dated campaign tag, and they travel with the analysis.
What the AI Is Told
Adjusted click figures are the truth. A raw click number is never called a win, and if raw and adjusted disagree, it says so.
Sales cannot be measured here, so clicks on a product link count as interest, never as revenue.
Do not propose rules that are already in force. Propose a new rule only when the evidence supports it, zero is a good answer, and each one must state its evidence.
Keep it short: an analysis of two paragraphs at most, no more than three recommendations, and a Telegram summary under 600 characters. Do not invent data.
Good to Know
The agent suggests but never decides. Proposed rules stay inactive until a person ticks Active in Airtable, and only then do they reach the writing prompts.
Each issue is scored once, because it is logged by its Kit broadcast ID and send date.
The summary lands in Telegram, the full write-up sits in Airtable, and every run is logged.
AI Post Rewrites and CTA Rotation for a Content Team
Two n8n workflows that rewrite a social post when a reviewer ticks Redo, and add the best call-to-action to each thread from a rotating library.
Section: n8n Automations
Purpose
A content team keeps its social post drafts in Airtable, with a version for X, LinkedIn and Threads. A reviewer checks each draft there. These two n8n workflows handle two jobs inside that review loop: rewriting a draft when the reviewer asks for changes, and adding the right call-to-action (CTA) to each thread.
The Solution
Both workflows read and write the same Airtable base, and both send their writing tasks to Claude through one shared sub-workflow.
Workflow 1: Redo (rewrites a draft from reviewer feedback)
The reviewer ticks the Redo checkbox on a card and writes what to change in the Feedback field.
n8n finds every card with Redo ticked and source text filled in. A dispatcher workflow can start it, and an hourly run catches anything that was missed.
It reads the current Post Rules and Craft Playbook from a settings record in Airtable. The team edits them there, and every new draft follows them first.
Claude gets the original source, the previous draft and the feedback. It applies the feedback exactly, keeps everything the reviewer did not mention, and returns three versions: the X post, a LinkedIn post of 600 to 1,100 characters, and a Threads post of about 420 characters (500 at most) that ends on a complete sentence.
A clean-up step removes hedging lines such as “the source does not establish…”. It also keeps the “shared by a publisher” credit line only on posts that carry an image, and moves it to the last line.
n8n writes all three drafts back to the card, unticks Redo and sets the status to Pending Review. Because Redo is cleared, each request is handled once. The feedback stays on the card as a record.
What the prompt enforces
Use only facts that appear in the source text or the previous draft, and never invent numbers, dates, figures or view counts.
Return the full revised post, not a list of changes.
If the feedback asks for a quote, keep the publisher’s own words word for word in quotation marks, and write any analysis separately after it.
Follow the account’s voice rules, with a list of banned words and AI-sounding phrases.
Workflow 2: CTA Auto-Insert (picks the call-to-action for each thread)
Every two minutes it looks for draft cards that have no CTA linked yet.
It reads every active CTA from a CTA library in Airtable. Each entry has the CTA text, link, category, weight, times used, conversions, a weekly cap and a “use when” note.
A code step drops CTAs that have reached their cap and scores the rest. Weight counts double, each conversion adds half a point, and each earlier use takes one point off. The list is sorted best first.
Claude receives the post, its source and the ranked list. It picks one primary CTA that fits the topic and a different secondary CTA, using the “use when” notes. When two fit equally well, it takes the higher-ranked one so no CTA is overused. It writes the reply in the account’s voice and keeps each link exactly as stored.
Another code step builds the thread. The lead post comes first, the primary CTA is the first reply, the rest of the thread follows, and the secondary CTA goes at the end. Each part is numbered 1/, 2/ and so on.
n8n saves the thread on the card and links the primary CTA record to it. A linked CTA means the card no longer matches the search, so it is not processed twice.
The Times Used counter goes up by one for the primary CTA, and for the secondary CTA when there is one, so the rotation stays fair.
Image: CTA Auto-Insert in n8n: a schedule trigger, an Airtable search, a read of the CTA library, a code step that ranks the CTAs, a prompt step, the Claude pick through a shared sub-workflow, a code step that builds the thread, and an Airtable update that links the CTA, followed by two branches that raise the Times Used counters.
Good to Know
The Redo workflow is live. The CTA workflow is switched off in this version, because checking every two minutes would use about 21,600 executions a month.
Both workflows have an exit rule that stops a card from being handled twice: Redo is unticked in the first, and a CTA is linked in the second.
The rules, the playbook and the CTA library all live in Airtable, so the team can change how posts are written and which CTAs are used without opening n8n.
Power BI: Turning Five Booking Systems Into One Revenue Engine
A governed Power BI model and two dashboards that replaced three days of manual owner-statement assembly with an automated daily refresh for a short-term rental portfolio.
BnB Heaven manages a portfolio of short-term rentals and boutique B&Bs across the UK. Bookings arrived from Airbnb, Booking.com, Vrbo and their own direct site; cleaning and turnovers lived in the ops tool; finance sat in the accounting system. Owner statements and the monthly board pack took three days to assemble by hand. This case study shows how we consolidated everything into one governed Power BI model with two dashboards, cutting reporting effort by roughly 90% and giving the team occupancy, rate and channel-profitability numbers they finally trust.
The Challenge
Before the project, 'how are we doing?' was a surprisingly hard question to answer. Three problems came up in every conversation with the team.
Bookings scattered across platforms: Revenue lived in Airbnb, Booking.com, Vrbo and the direct site, with turnovers in the PMS and money in accounting. Nobody could see occupancy, nightly rate and net revenue in one place without exporting several files first.
Owner statements ate three days a month: A manager rebuilt every owner statement and the board pack by hand (copy, paste, reconcile, repeat). By the time they were ready the numbers were weeks old, and one slip meant redoing the lot.
No shared version of the truth: Channel profitability after platform fees was invisible. It wasn’t clear which channels actually paid off once commissions were taken out, so pricing and listing decisions were made on gut feel.
The Approach
Rather than bolt a chart onto each platform, we built one properly modelled layer underneath a small number of focused dashboards. Four phases over six weeks:
Discovery & metric definitions (week 1): Workshops to agree exactly what occupancy, ADR, RevPAR and net-of-fees revenue mean (one signed-off definition each, so the model has a single source of truth).
Connect & clean with Power Query (weeks 2–3): ETL into all five sources, standardised property and channel keys, and consistent handling of platform fees, refunds and cancellations so figures reconcile to the accounts.
Star-schema model & DAX (weeks 3–4): A clean fact-and-dimension model with a shared date table, reusable DAX measures (occupancy, ADR, RevPAR, YoY, target) and row-level security so each owner sees only their own properties.
Dashboards, refresh & handover (weeks 5–6): Two role-based dashboards, an on-premises gateway for the automated 06:00 refresh, plus training and a short written guide so the team can extend the model themselves.
The Solution
Executive overview
The board-level view answers leadership’s core questions on one screen: where revenue comes from, how occupancy and rate are trending, which properties and channels pull their weight, and whether the year is on target.
Image: Revenue & Occupancy: the executive overview page, live in Power BI Desktop, with KPIs, revenue trend, channel mix, property and location breakdowns and target attainment.
Operations & guests
The operations view is a daily working screen for the wider team: booking volume against occupancy, manager contribution, the direct-site booking funnel and guest satisfaction by location.
Image: Operations & Guest Experience: bookings vs occupancy, manager revenue, booking funnel and 5-star reviews by location.
Cumulative revenue vs operating cost shows the margin gap at a glance.
Channel donut exposes reliance on commission-heavy platforms vs direct.
Top-property table ranks by revenue with live YoY growth flags.
Gauge tracks progress against the £4.55M annual target (106%).
Under the Hood
Layer
What We Built
Tools
Sources
Airbnb, Booking.com, Vrbo, direct site, PMS / channel manager, accounting
Row-level security by owner / manager, documented definitions
RLS roles
Delivery
Two role-based dashboards, automated daily refresh
Power BI Service + gateway
The Results
+5.6pts: Occupancy rate
+0.14: Guest rating
3 days → 0: Manual reporting
106%: Of annual target
Measure
Before
After
Owner statements & board pack
~3 days, manual
Automatic, ready by 06:00
Data freshness
~3 weeks old
Refreshed daily
Channel profit (net of fees)
Invisible
Live, per channel
Sources reconciled
Manually, error-prone
Reconciled to accounts automatically
With channel profitability visible net of platform fees, BnB Heaven leaned into direct bookings and trimmed commission-heavy inventory, finishing the year 6% ahead of target.
Telegram Thread Builder: From Dropped Screenshots to a Reviewed X Thread
An n8n workflow that turns a source text and screenshots dropped in a Telegram chat into a ready-to-review X thread, with automatic image blurring, AI writing and a card created in Airtable.
Section: n8n Automations
Purpose
A content team collects ideas in a Telegram group chat: a piece of source text, plus screenshots of results. This workflow lets them turn that material into an X thread, or a single post, without leaving Telegram. The finished draft lands in Airtable as a card for review.
The Solution
The chat works like a conversation. A person opens a session, drops the material, answers a few short questions, and the workflow builds the thread. Every message in the chat is saved in Airtable against the session, so nothing is lost between steps.
Step 1: Open a session
Someone replies “Go” to a dropped text or image. They can add a format at the same time: thread, image, text or long.
The bot answers with the steps. Drop the images in order, where a caption under an image becomes the text for that post. Send any extra text. Send “done” when finished, or “cancel” to stop.
Telegram splits long texts into several messages. The workflow merges those parts back together in the right order.
Each image and text is saved in Airtable as its own row, linked to the session. A session stays open for 12 hours.
Step 2: Answer the setup questions
When “done” arrives, the bot asks only what it does not know yet. These are numbered questions, so a reply is just a number: how many posts, which call-to-action and on which post, and whether to blur the images.
Writing “go” at any point accepts the defaults for everything still open. Tags typed with the first “Go”, such as “no blur” or “cta newsletter”, skip those questions.
The bot ends with a one-line summary, and a reply of 1 starts the build. If nobody answers for 10 minutes, the workflow carries on by itself with the defaults.
Step 3: Prepare and blur the images
The workflow collects the session’s images in order, up to 15, and downloads each one from Telegram.
Each image goes to a blur service that hides page names, logos and faces.
A check then confirms that every image came back blurred. If any image fails, no thread is created. The session is reopened, and the bot says which images failed and why, so the team can send “done” again.
Step 4: Write the thread
Claude receives the full source text, the team’s captions and notes, every blurred image, and the current Post Rules from Airtable. It reads the numbers and labels inside the screenshots as part of the source.
The prompt fixes the number of posts, puts one image on each post in order, and asks Claude to use every fact from the material.
It also sets the voice: a long opening hook, replies that each carry one step of the story, and an analysis near the end. The posts must not name any page, person or brand shown in the material.
Step 5: Clean up, add CTAs and deliver
A code step cleans the writer’s output. It removes links, handles, “via” credits, post numbering and stock phrases, and it keeps the thread to the agreed number of posts.
It checks that every image is used exactly once, in order, and spreads them again if the writer got that wrong.
Call-to-actions are added from the CTA library in Airtable, for example a newsletter reply at the end, or a consulting reply placed at post 4.
n8n creates the card in Airtable with status Pending Review, the hook and the full thread, and attaches each blurred image. It marks the session done.
The bot replies in Telegram with the post count, the number of images, the CTA used and a preview of the hook, plus a link to the card so the team can check the blur and approve.
Image: The Telegram thread session workflow in n8n: messages come in on the left and are routed by what they mean (start, image, note, cancel, done, ask, single post). The long middle row builds the thread, from blurring the images to the Claude writer, the Airtable card and the Telegram summary. The rows below handle the setup questions with the 10-minute timer, and single-post sessions.
Single Posts
The same chat also handles posts that are not threads. After “Go”, the bot asks the shorter set of questions. The workflow then merges the text, captions, first image and chosen CTA into one request and hands it to the team’s existing intake workflow, so single posts use the same writer, blur and card steps as everything else.
Built-in Safeguards
An unblurred image is never attached, unless the team asked for no blur.
When the blur or the writer fails, the session is reopened and the team is told in Telegram, so they can retry with “done” instead of starting again.
If the writer returns something unusable, no card is created.
An image that fails to upload to the card is reported in the summary message.
Good to Know
This is a large workflow of about 65 nodes, which is why the diagram is wide. It is read from left to right, and each row has one job.
The Post Rules, the CTA library and the session rows all live in Airtable, so the team can change how threads are written without opening n8n.
Monday.com board setup built for Veterinary Specialist Partners’ operations.
Section: Monday.com Setups
Introduction
Veterinary Specialist Partners (VSP) is a specialized veterinary hospital network with facilities in Louisville and Evansville, known for its expert handling of complex medical conditions in pets. Before automating their operations, VSP managed appointments, patient records, and inter-departmental communications manually, which often led to inefficiencies and potential delays in patient care.
To enhance operational efficiency and patient service delivery, VSP partnered with our team to transition from a fully manual system to an automated solution using monday.com, aiming to streamline processes from appointment booking to patient follow-ups and referrals management.
Background
The hospital faced significant operational challenges:
Manual Appointment Scheduling and Management: Dependent on phone and in-person scheduling, causing bottlenecks and increased administrative overhead.
Fragmented Patient Records Management: Dispersed patient histories and treatment records across various platforms, complicating data retrieval and management.
Inconsistent Follow-Up Communications: Manual processes for scheduling follow-ups and managing patient reminders were error-prone and inefficient.
Objectives
The main objectives for implementing monday.com were to:
Automate Appointment Scheduling and Management: Streamline the entire appointment lifecycle, from booking through cancellation.
Centralize Patient Data: Create a unified system for managing patient histories and treatment records.
Automate Communication: Enhance communication with patients regarding appointments and follow-ups.
Solutions Implemented
Monday.com Integration and Automation
Automated Appointment System
Automated Appointments: Every new appointment booked is automatically captured and added to a specific monday.com board designated for appointments. Separate groups are maintained for each type of appointment and location.
Patient Registration and Check-In: Automated processes for patient registration and check-in ensure that all necessary patient information is captured and stored in monday.com, linked directly to their respective appointment entries.
Integration with External Scheduling Tools
Calendly Integration: Direct integration with Calendly allows seamless synchronization of appointment bookings and cancellations, updating monday.com boards in real-time.
Email and SMS Reminders: Automated email and SMS reminders are set up to notify patients about upcoming appointments, using integration with email platforms and SMS gateways.
Patient Records Management
Centralized Records: All patient records are consolidated within monday.com. Custom forms are used to collect and upload medical histories, treatment records, and other relevant documents directly into the system.
Access Control: Different access levels are set for various team members, ensuring sensitive patient information is handled securely and in compliance with medical privacy regulations.
Task Automation for Follow-Ups and Referrals
Follow-Up Automation: Automated tasks are created for follow-ups, ensuring patients receive timely care post-appointment. These tasks are linked to specific appointments for easy tracking.
Referral Management: A dedicated board manages referrals to and from other specialists. Each referral triggers specific workflows to ensure no patient is overlooked and all records are shared with relevant parties.
Outcomes and Results
Improved Operational Efficiency
Streamlined Appointment Handling: Reduction in administrative workload and minimized errors related to manual appointment management.
Centralized Data Management: Unified patient records enhanced data-driven decision-making, enabling better patient care and easier compliance with healthcare regulations.
Enhanced Communication
Reliable Patient Reminders: Decreased no-show rates through timely, automated reminders.
Improved Referral Coordination: Strengthened relationships with other healthcare providers and ensured comprehensive patient care through efficient referral management.
Technical Highlights
monday.com Customizations: Extensive use of custom fields, automation rules, and integrations tailored the workflow specifically to VSP’s needs.
Integration Suite: Seamless integration with external tools like Calendly, email platforms, SMS gateways, and medical record software to centralize operations and communication.
Future Steps and Sustainability
Ongoing Optimization: Continual refinement of the system based on user feedback and evolving needs.
Expansion of Automation: Exploring further automation opportunities to enhance other areas of hospital operations.
Conclusion
The implementation of monday.com revolutionized VSP’s operations, enabling them to deliver exceptional care with greater efficiency and precision. This case study demonstrates the transformative impact of customized automation solutions in complex healthcare settings, proving monday.com’s capability to handle intricate workflows and extensive data management.
ClickUp: Automating Operations for Veterinary Specialist Partners
ClickUp setup used to automate operations for a veterinary specialist partnership.
Section: ClickUp Setups
Introduction
Veterinary Specialist Partners (VSP) is a specialized veterinary hospital with locations in Louisville and Evansville. The hospital is renowned for its expert management of complex medical diseases in pets, ranging from autoimmune disorders and various cancers to critical care involving advanced surgical procedures. Prior to the implementation of automation, VSP managed appointments, patient records, and inter-departmental communications manually, leading to inefficiencies and potential delays in patient care.
Recognizing the need to enhance their operational efficiencies and patient service delivery, VSP partnered with our team to transition from a fully manual system to a fully automated one, streamlining everything from appointment booking to patient follow-ups and referrals management.
Background
The hospital faced several operational challenges:
Manual Appointment Scheduling and Management: Reliance on phone and in-person scheduling led to bottlenecks and increased administrative overhead.
Fragmented Patient Records Management: Patient histories and treatment records were dispersed across various platforms, complicating data retrieval and management.
Inconsistent Follow-Up Communications: Manual processes for scheduling follow-ups and managing patient reminders were prone to human error and inefficiency.
Objectives
The primary goal was to automate critical aspects of VSP’s operations, including:
Appointment Scheduling and Management: Automate the entire appointment lifecycle, from booking to cancellation.
Patient Data Centralization: Create a unified system for managing patient histories and treatment records.
Automated Communication: Streamline communication with patients regarding appointments and follow-ups.
Solutions Implemented
ClickUp Integration and Automation
Calendly to ClickUp Integration
New Appointments: Automatically captures every new appointment booked via Calendly and adds it to ClickUp. Distinct lists for different appointment types are maintained for both Louisville and Evansville locations.
Tags and Lists: Appointments are tagged with specific descriptors (e.g., new appointment, recheck appointment) and sorted into corresponding lists in ClickUp to ensure easy access and organization.
JotForm Integration
Form Submission Tracking: Monitors patient compliance with required JotForm submissions post-appointment scheduling. If a form is not submitted, automated reminders are sent via email and SMS two days before the appointment to ensure completion.
Medical Information Integration: Captures and integrates patient medical information from JotForm into ClickUp, linking it directly with the patient’s appointment record for streamlined access.
Lab Results Automation
Email Integration: Automatically imports email communications and attachments from associated labs directly into ClickUp. This ensures that all pertinent lab results are accessible within the patient’s specific task, eliminating the need for manual email checks and data entry.
Appointment Cancellation Handling
Automated Cancellation Management: If an appointment is canceled in Calendly, the system automatically removes the appointment from the active list and moves it to a ‘Cancelled Appointments’ list. This helps in maintaining accurate and up-to-date appointment statuses without manual intervention.
Centralized Appointment Management
Unified Appointment List: A master list called ‘Appointments’ captures and consolidates every appointment from both Louisville and Evansville, regardless of type. This list serves a referral department that requires visibility across all bookings.
Cross-List Synchronization: Changes made in any appointment’s custom fields are dynamically updated across both location-specific and centralized appointment lists. This ensures consistency and accuracy in appointment details across the board.
Past Appointment Pruning
Automated Cleanup: The system automatically removes appointments whose dates have passed from all lists in ClickUp, helping to keep the database current and focused only on upcoming and actionable appointments.
Enhanced Scheduling and Reminders
RingCentral Integration: Utilizes RingCentral to send automated SMS reminders about upcoming appointments, reducing no-show rates and ensuring patients are adequately prepared for their visits.
Dropdown Functionality for Cancellations: Enables staff to cancel appointments directly in ClickUp via a dropdown field change, which then reflects in both ClickUp and Calendly, streamlining the process.
Outcomes and Results
Improved Operational Efficiency
Streamlined Appointment Management: Reduced administrative burden and errors related to manual appointment handling.
Centralized Data Management: Achieved a unified view of patient and lab data across all locations, enhancing data-driven decision-making.
Enhanced Communication
Automated Patient Reminders: Improved patient preparation and reduced no-shows through timely automated reminders.
Referral Hospital Coordination: Strengthened relationships and improved data sharing with referral hospitals.
Technical Highlights
ClickUp Customizations: Leveraged custom fields, lists, and automation to create a tailored workflow management system.
Integration Suite: Seamlessly integrated with Calendly, JotForm, Gmail, RingCentral, and Slack to centralize operations and communications.
Future Steps and Sustainability
Continue refining the integration based on user feedback and system performance.
Explore additional automation opportunities to further enhance efficiency and patient care.
Screenshots
Conclusion
The implementation of ClickUp and the integration of essential tools have revolutionized Veterinary Specialist Partners’ operations, positioning them to continue delivering exceptional care with enhanced efficiency and accuracy. This case study illustrates the power of tailored automation solutions in solving complex operational challenges in healthcare environments.
An event-driven Python service (FastAPI, Celery, Playwright and Claude) that answers load quote requests from a shared inbox with three ranked carriers. Median first reply: 48 seconds, down from 7 minutes 40.
Load quote requests landed in a shared quotes@ inbox, around 40 a day. A dispatcher would read the request, log into the carrier portal (no public API for what they needed), check which trucks were available on that lane, cross-reference an internal rate sheet, and reply with three carriers and a price. Six to nine minutes each, every time.
Their first automation attempt was a Python script on a five-minute cron driving a hosted agent platform. It worked about 70% of the time, cost roughly $0.42 per request, and failed silently. They usually found out from a customer, not a log.
Three root causes, and all three shaped the rebuild:
Polling. A 0–5 minute delay before anything started, and the cron had no idea what the previous run had done.
Re-planning on every run. The agent asked an LLM to navigate the portal from scratch each time, so any UI jitter became a fresh failure mode, and every run paid full navigation token cost.
No durable state. Nothing recorded which requests had been handled, so retries either duplicated replies or never happened.
The architecture
Everything runs as a small Python service on a single Fly.io machine, a FastAPI app that receives Gmail push notifications, and a Celery worker pool that does the actual work.
Image: Event-driven quote triage pipeline: a FastAPI webhook verifies and acknowledges the push, the request is de-duplicated and queued (Postgres key, Redis queue), Browserbase and Playwright check portal availability while an Airtable rate sheet supplies lanes, margins and exclusions, Claude ranks the top 3 carriers, and a threaded reply goes out on Gmail and Slack.
Key engineering decisions
Real-time trigger, not a cron. Gmail's users.watch publishes to a Google Pub/Sub topic, which pushes to a FastAPI endpoint. The handler does three things and nothing else: verify the JWT on the push request, write the message ID to Postgres, and return 204. Pub/Sub redelivers anything not acked within 10 seconds, so all real work is handed to Celery and the endpoint stays under 50 ms.
@app.post("/gmail/push")
async def gmail_push(req: Request, claims=Depends(verify_pubsub_jwt)):
envelope = await req.json()
history_id = decode_message(envelope["message"])["historyId"]
for msg in gmail.list_since(history_id):
if requests_repo.claim(msg.thread_id): # INSERT ... ON CONFLICT DO NOTHING
process_quote.delay(msg.thread_id)
return Response(status_code=204)
Idempotency lives in the database, not in memory. claim() is an INSERT ... ON CONFLICT DO NOTHING on the Gmail thread ID. If Pub/Sub redelivers, or the safety net and a live webhook race each other, exactly one worker wins the row. This is the piece that eliminated duplicate replies outright.
Cached navigation instead of per-run LLM planning. Playwright drives a Browserbase session with stored credentials and a persistent context. The first time the portal layout is seen, an LLM produces a navigation plan, a sequence of selectors and actions, which is hashed against the page structure and cached in Postgres. Every subsequent run replays the cached plan with plain Playwright calls: no LLM, no tokens, sub-second steps. Only when a replay step fails does the code fall back to re-planning, write the new plan, and post a Slack notice that the portal changed. That's the same idea as Skyvern's Route Memorization, built explicitly so the failure path is visible rather than silent.
Prompt caching on the ranking call. The system prompt, the scoring rubric, and the serialised carrier roster are static across requests and sit in a cached block; only the parsed request and the fresh availability data go in the user turn. Roughly 11k stable input tokens moved to cache reads.
Concurrency capped deliberately. The Celery pool runs three workers. The portal rate-limits aggressively and had previously locked the shared account twice, so throughput was traded for account safety.
State machine and the safety net
Every request row moves through an explicit status column. The hourly sweep is the belt-and-braces layer for the rare case where a Pub/Sub push is lost or a worker dies mid-run.
Image: Request states and recovery: a request is processing (row claimed, timestamp set), then done (reply sent, thread id) on success or failed (alert posted to Slack) on error. An hourly sweep re-runs rows stuck for more than 15 minutes.
The sweep is a Celery Beat task, about 30 lines: select rows in processing with claimed_at < now() - interval '15 minutes', reset the attempt counter, re-enqueue. After three attempts it moves to failed and posts a Slack alert with the thread link and the last exception, so a dispatcher can handle it manually and an engineer can see exactly where it broke.
Errors are handled in three tiers rather than one generic retry: transient network and portal timeouts retry with exponential backoff inside the task; a failed selector replay triggers re-planning and a "portal layout changed" notice; anything else fails the row loudly.
Results
Before
After
Time to first reply (median)
7 min 40 s
48 s
Cost per request
$0.42
$0.06
Success rate (30-day)
71%
98.6%
Duplicate replies / week
4–6
0
Silent failures
routine
none, every failure alerts
The cost drop came from two places roughly equally: cached navigation removed the per-run planning tokens, and prompt caching cut input cost on the ranking call.
Delivered
Dockerised FastAPI + Celery service with a one-command deploy; Postgres schema and migrations; the Browserbase automation module with cached-plan replay and fallback; the Claude ranking module with the rubric prompt; Slack alerting; a runbook covering the four common failure modes and how to re-run a request by thread ID; and a 45-minute handoff call testing a live request end to end.
A Python pipeline (Prefect, Skyvern and GPT-4.1) that quotes all 22 approved suppliers for every ingredient reorder and delivers a ranked comparison in about four hours instead of 3.5 days.
Section: Python Automation
At a glance
Mid-size generic drug manufacturer, ~40 SKUs in production. Five weeks. Python 3.12, Prefect, Skyvern Cloud, OpenAI GPT-4.1, Postgres, S3, Slack. Went from quoting a fifth of their supplier base per order to quoting all of it.
The 6% nobody could see
Production planning flags a reorder for an active ingredient. A procurement officer opens a portal, searches the CAS number, pulls a price, downloads the spec sheet, pastes it into Excel. Then the next portal. Then the next.
Twenty-two suppliers were on the approved list. In practice the officer got through five or six before the purchase order had to go out, and the same five or six every time, the ones with the least annoying logins.
The waste wasn't the officer's afternoon. It was the sixteen vendors nobody had time to ask. When the full list was finally quoted, the cheapest compliant supplier turned out not to be one of the usual five surprisingly often.
Why this wasn't a scraping job
Three things ruled out the obvious approach, and a hand-written Playwright suite had already been abandoned for exactly these reasons.
Vendors don't agree on what anything is called. Metformin HCl USP, metformin hydrochloride BP/EP, or a bare product code, same molecule, three listings, no shared key except the CAS number buried somewhere on the page.
Quotes don't arrive in one shape. Some portals render a price table. Some hand back a PDF. Some won't tell you anything until you've submitted an RFQ form and waited for an email.
And it's a regulated purchase. Pharmacopoeia grade, GMP status, and whether the supplier holds a DMF or CEP are pass/fail gates, not preferences. A cheap quote from a vendor without the right filing isn't a cheap quote. It's not a quote at all. Every choice has to survive an audit two years later.
Following one RFQ through
Image: Multi-vendor ingredient quoting pipeline: RFQ intake (CAS number, grade, quantity), a Skyvern browser handles login, 2FA and CAPTCHA, prices come from an in-page table or from a downloaded and parsed PDF or emailed quote, GPT-4.1 normalises and validates them, and the ranked comparison goes to a Slack thread with an audit log.
A reorder request arrives as a CAS number, a grade, and a quantity. Before anything opens a browser, that gets resolved against an internal ingredient register, deterministically, on the CAS number, never on the product name.
Prefect then fans the request out to twenty-two isolated subflows, one per vendor. Each launches its own Skyvern cloud browser and logs in. Nine of these portals want 2FA and two sit behind CAPTCHAs; that's handled inside the login step rather than being a per-vendor engineering problem.
From there the paths diverge by portal. Where the price sits in the page, it's read straight off the page against a schema. Where it only exists as a PDF or an emailed quote sheet, the file is downloaded and parsed separately.
Everything then funnels back into a single normalisation pass, one model call, one strict schema, twenty-two vendors reduced to comparable rows: unit price, currency, unit of measure, MOQ, lead time, incoterm, grade, GMP status, DMF reference.
The ranked comparison lands in Slack about four hours later, with every row linking to the evidence behind it.
Four forks in the road
Skyvern rather than raw Playwright
The rejected option was writing and maintaining twenty-two scripts. That's what had already failed. At this many portals somebody is always redesigning something, and the maintenance load quietly exceeded the hours the automation was saving. Buying the resilience rather than building it was the entire reason the project was viable a second time.
1Password rather than a second vault
Skyvern can read credentials from several providers, and the client was already on 1Password. The alternative, standing up a dedicated secrets store, would have meant twenty-two sets of credentials living in two places, which is worse security dressed up as better engineering.
Selectors first, model second
Every stable element on a page gets a selector and a prompt. The selector runs, costs nothing, and usually works; the model only engages when it misses. Going model-first on everything would have been simpler to write and roughly ten times the running cost.
The model never decides what an ingredient is
It was tempting to let a model reconcile vendor product names. It isn't allowed to. Where a listing has no CAS field, the model may suggest a match, and that suggestion goes to a human. Misidentifying an ingredient in this industry isn't a data quality issue, and no accuracy figure would have made that trade acceptable.
When it breaks
Image: Route Memorization in practice: a memorized route replays deterministically. If the replay works the quote is captured with its evidence hash; if the route breaks it is regenerated by AI with a diff alert; if it keeps failing it goes to a review queue with the session recording attached.
Skyvern's Route Memorization compiles a successful run into a deterministic script and replays it. When a portal changes and the replay fails, it re-plans with AI, finds the new path, and regenerates the script. That mechanism does the heavy lifting here.
What it doesn't do is announce itself. A regenerated route is a silent event, and silent events don't belong in a regulated purchasing process.
So the wrapper watches for it. Each subflow records its action count and wall-clock duration; a memorised replay sits in a tight, predictable band, and an AI re-plan blows straight through it. That signal tags the run as a regeneration, diffs the new route against the old one, and drops a summary into Slack so someone can see what the portal changed before the next cycle.
If a regenerated route produces a quote that can't be evidence-checked, that vendor goes to a review queue with its session recording attached instead of into the comparison. About 4% of quotes land there, mostly from two portals that render prices as images.
The result is that failure is local. One portal breaking means twenty-one quotes still land and the RFQ is still useful.
Turning daily ticketing PDFs and emails from multiple vendors into one automated revenue dashboard for a music festival organizer.
Section: Python Automation
Overview
Palm Tree Crew, a live music festival organizer, needed accurate daily revenue and attendance numbers across several cities. We built a custom Python pipeline on Google Cloud that reads vendor audit emails, applies the client’s business rules, stores everything in BigQuery and renders an executive dashboard in Google Sheets with no manual steps.
The business problem
Every day the operations team downloaded static PDF audits from Ticketmaster, Front Gate Tickets and Posh. They split passes by hand (a 2-Day Pass counts toward both Friday and Saturday) and typed the numbers into a master Google Sheet. The work took hours and invited mistakes. VIP Table and Corporate Sponsorship revenue lived in separate spreadsheets, so leadership never saw total gross and remaining capacity in one place.
The solution
A cloud-hosted ETL pipeline runs on a schedule. It intercepts vendor emails through the Gmail API, parses PDF and HTML reports with custom Python code, calculates the client’s metrics, loads the results into Google BigQuery and rebuilds a styled Google Sheets dashboard. Offline revenue from other spreadsheets is linked in through a manual configuration area.
Project snapshot
Item
Detail
Client
Palm Tree Crew
Industry
Live events and entertainment (music festivals)
Language
Python, written as custom code
Cloud
Google Cloud Run and Cloud Scheduler
Data warehouse
Google BigQuery
Integrations
Gmail API, Google Sheets API
Ticketing sources
Ticketmaster (PDF), Front Gate Tickets (HTML email), Posh
How the System Works
The pipeline runs once per reporting cycle with no human steps, from vendor email to finished dashboard.
Step
Process
1. Scheduled start
Google Cloud Scheduler triggers the Python service on Cloud Run.
2. Email ingestion
The Gmail API, with secure OAuth, picks up the daily audit emails from each ticketing vendor.
3. Parsing
Vendor-specific parsers read the data: a PDF scraper for Ticketmaster, an HTML table parser for Front Gate Tickets.
4. Business logic
Code applies the client’s rules: the 2-Day Rule, wrap rates and capacity detection.
5. Warehouse load
Results are written to BigQuery in three tables, so every ticket sold keeps an audit trail.
6. Dashboard render
The Google Sheets API rebuilds a formatted executive dashboard, including linked offline revenue.
Architecture
Image: Vendor emails enter through Gmail, the Python service on Cloud Run parses them and applies the client’s rules, BigQuery keeps the history, and the Google Sheets dashboard shows the result.
Offline VIP and sponsorship sheets are linked into the dashboard with IMPORTRANGE.
Core Pipeline Components
The pipeline is custom Python code split into four parts, each with one job.
Automated ingestion and parsing
The Gmail API with secure OAuth intercepts the daily audit emails from each vendor. Bespoke parsing engines handle the unstructured formats:
Ticketmaster: a custom PDF scraper that bypasses AWS API Gateway redirects, reads complex tabular data and adjusts to unannounced layout changes by the vendor.
Front Gate Tickets: an HTML parser that extracts inline tables directly from the email body.
Business logic
The code applies the client’s own rules instead of only moving data:
The 2-Day Rule: splits a multi-day pass into the correct daily attendance buckets, for example one 2-Day Pass counts toward both Friday and Saturday.
Wrap rates: computes 24-hour sales velocity.
Dynamic capacities: detects and adjusts venue capacity limits from live vendor reports, so no hardcoded limit goes out of date.
Data warehouse
All parsed data goes into Google BigQuery to keep full fidelity and prevent double-counting. Three tables hold the history of every ticket sold:
Table
Purpose
raw_reports
The original report as received from each vendor.
report_lines
The individual lines parsed out of each report.
daily_snapshots
The calculated daily position used by the dashboard.
Dynamic dashboard rendering
The final step renders an executive-ready Google Sheets dashboard. The code formats the layout, freezes rows, color-codes stale data feeds and calculates multi-day averages.
A #MANUAL configuration area lets non-technical team members link external spreadsheets with =IMPORTRANGE() for offline VIP Table and Sponsorship revenue. The dashboard syncs this data automatically, so total top-line revenue appears in one view.
Product Experience
The executive team sees one Google Sheets dashboard that is rebuilt automatically from the BigQuery data. It formats itself, freezes the header rows, color-codes feeds that have not updated and shows multi-day averages, daily wrap rates and remaining capacity per show.
Results & Impact
The daily reporting cycle went from hours of manual transcription to no manual steps. The report gives no counts of events, hours saved or revenue, so none are stated here.
Area
Result
Manual data entry
None. The pipeline runs on Google Cloud Scheduler.
Data accuracy
Transcription errors and arithmetic mistakes removed. The dashboard is the single source of truth for executives.
Revenue view
Online ticket sales and offline VIP and sponsorship revenue in one top-line view.
Onboarding new shows
A standard New Show Onboarding Template lets the client add a festival or ticketing platform in minutes.
Decision support
Real-time capacity and daily wrap rates inform ad spend and tier pricing.
Technical Challenges & Solutions
Unstructured vendor formats
Challenge
Ticketmaster sends static PDFs behind AWS API Gateway redirects, and Front Gate sends tables inside email bodies.
Solution
A custom PDF scraper that follows the redirects and adapts to layout changes, plus an HTML parser for inline tables.
Outcome
Both vendors feed the pipeline without manual downloads.
Business rules that were done in the operator’s head
Challenge
A 2-Day Pass had to be split across Friday and Saturday by hand.
Solution
The 2-Day Rule, wrap rates and capacity detection are coded into the pipeline.
Outcome
Consistent daily buckets with no mental math.
Outdated hardcoded limits
Challenge
Venue capacities change, and fixed numbers in code go stale.
Solution
Capacities are read from live vendor reports.
Outcome
The dashboard shows current remaining capacity.
Double-counting and audit trail
Challenge
Daily reports overlap, so repeated loads could count a ticket twice.
Solution
A three-table BigQuery design (raw_reports, report_lines, daily_snapshots) keeps the original data and the calculated snapshots separate.
Outcome
A historical record of every ticket sold.
Offline revenue in separate sheets
Challenge
VIP Tables and Sponsorships were tracked outside the main sheet and maintained by non-technical staff.
Solution
A #MANUAL configuration area where staff link external sheets with =IMPORTRANGE().
Outcome
Offline revenue appears in the dashboard without code changes.
Key Takeaways & Future Development
Key takeaways
End-to-end automation: email ingestion, parsing, calculation, storage and reporting run without human input.
Custom code where vendors give no API: parsing PDFs and email tables made the data reachable.
Built for scale: a decoupled design and an onboarding template let the client add shows and platforms quickly.
Future development
Add parsers for further ticketing platforms as the client adds vendors.
Add alerts when a vendor email is missing or a report layout changes.
Extend the dashboard with ad-spend data to compare against sales velocity.
AI-Powered Construction Report SaaS (MeinBüromeister)
Turning construction-site voice notes, photos, and messages into structured reports through an automated AI-powered backend for German trade companies.
Section: Custom SaaS Platforms
Overview
MeinBüromeister is a B2B SaaS platform for German construction and trade companies. It allows workers to submit job updates through WhatsApp instead of preparing every report manually. The backend organizes incoming information, generates structured reports, and asks workers to review them before submission.
The Business Problem
Construction workers often send separate voice notes, photos, and text corrections from job sites. Office staff must identify the correct customer and project, organize materials and working hours, and format the information into professional reports. This creates repetitive administrative work and makes report preparation more time-consuming. The goal was to reduce this manual effort while keeping workers involved in reviewing and approving their reports.
The Solution
An n8n-based backend receives WhatsApp messages and groups related inputs into a single session. OpenAI Whisper transcribes voice recordings, while Claude AI extracts relevant report information into structured data. After the worker reviews and approves the report, the backend stores it in Supabase for delivery to the office dashboard. Supporting workflows automate customer onboarding, subscription management, team access, and error handling.
Project Snapshot
A production backend connecting WhatsApp messaging, AI-powered report extraction, worker approval, report storage, customer onboarding, and subscription lifecycle management.
How the System Works
System Architecture
The backend is organized around an n8n automation layer that sits between WhatsApp, the AI services used for transcription and extraction, and the Supabase/Stripe infrastructure that powers the office dashboard.
Supporting Integrations
Stripe: Checkout, subscription lifecycle events, plan changes, and subscription-based access rules.
Supabase Auth: Authentication support for the application.
Supabase Storage: Storage for uploaded photos and other media.
Docker: Supports the self-hosted deployment environment.
Core Automation Workflows
The backend is built around interconnected n8n workflows that automate WhatsApp message handling, AI-powered report generation, customer onboarding, and subscription management. Each workflow handles a specific responsibility and integrates with the relevant APIs and backend services to support the complete reporting lifecycle. Some main automation workflows are shown below.
1. Core Onboarding & Workflow Management
This workflow handles company onboarding, setup-code verification, worker registration, subscription checks, report approval, project management, and notifications through interconnected n8n automation nodes.
Image: Core onboarding and workflow management: company setup, worker registration, subscription checks, report approval, project handling, and notifications across 15 interconnected automation groups.
2. AI-Powered Report Processing Workflow
This workflow processes WhatsApp messages using Whisper and Claude AI to extract structured report data, enrich customer and project details, validate reports, and handle database updates and notifications.
Image: AI-powered report processing: message intake, voice transcription, Claude-based data extraction, report generation, and save/notification handling.
Product Experience
Report inbox
Image: The office inbox organizes incoming reports by date, technician, customer/project, category, and status.
Customers and projects
Image: The project view groups customers and highlights open-report counts and recent work.
WhatsApp work history
Image: Workers review and approve each AI-generated report directly in WhatsApp before it goes to the office.
<30s: Voice note to structured report
Image: Once approved, the report is saved, confirmed back to the worker on WhatsApp, and delivered to the office as a PDF.
Core Backend Capabilities
AI Report Extraction: Supports four report categories (Rapport, Aufmaß, Neues Angebot, and Notiz). The AI extracts work descriptions, materials, dimensions, and working hours while using prompt guardrails to reduce invented details.
Multi-Input Session Handling: Groups related voice notes, photos, captions, and corrections into a single processing session instead of treating every incoming message as a separate report.
Project Matching and Clarification: Uses available employee and project records to associate reports with the correct destination. When a match is ambiguous, the workflow can request clarification.
Onboarding and Access Management: Supports company setup, worker invitations, registration tracking, and access checks to ensure incoming messages are routed correctly.
Subscription Lifecycle Management: Handles relevant Stripe events and subscription states, including active, canceling, and canceled subscriptions. It also manages plan mapping and seat limits.
Failure Handling and Validation: Validates incoming data and AI-generated output, handles incomplete sessions and invalid actions, and provides controlled paths for processing failures.
Results & Metrics
5: Production workflows
5: Testing rounds
30+: Test scenarios per round
25+: Bugs found & fixed
The project delivered five production workflows connecting WhatsApp, AI-powered report extraction, Supabase, and Stripe. Across five rounds of end-to-end testing, more than 25 bugs were identified and fixed, with over 30 test scenarios executed per round.
Technical Challenges & Solutions
During development and end-to-end testing, several integration and AI reliability issues required targeted solutions.
1. Stripe Subscription Data Handling
Challenge
The Stripe checkout event did not always contain all the subscription information required by the workflow.
Solution
Retrieved the complete subscription details directly through the Stripe API before continuing with subscription processing.
Outcome
Improved the reliability of subscription data handling.
2. Subscription Plan Mapping
Challenge
Mapping subscription plans based on payment amounts could lead to incorrect plan identification.
Solution
Used Stripe product IDs to identify plans instead of relying on monetary amounts.
Outcome
Made plan identification more consistent across subscription events.
3. AI Hallucination Control
Challenge
Claude AI could occasionally introduce materials or details that were not present in the worker’s input.
Solution
Refined the AI prompt with guardrails to discourage unsupported details and keep extraction grounded in the supplied information.
Outcome
Improved the consistency of structured report generation.
4. Supabase PATCH Response Handling
Challenge
Some Supabase PATCH operations returned empty responses, creating difficulties for downstream workflow nodes.
Solution
Explicitly referenced upstream node outputs where required instead of relying on the PATCH response to provide all subsequent data.
Outcome
Made downstream data handling more reliable.
5. End-to-End Integration Testing
Challenge
The system depended on multiple connected services, including WhatsApp, AI models, Supabase, and Stripe, making integration failures difficult to isolate.
Solution
Conducted five rounds of end-to-end testing, covering more than 30 scenarios per round, and fixed over 25 identified bugs.
Outcome
Improved integration reliability across the production workflows.
Key Takeaways & Future Development
Key Takeaways
Workflow automation: Built and integrated five production n8n workflows connecting WhatsApp, AI services, Supabase, and Stripe.
AI reliability: Improved structured report extraction through prompt engineering, validation, and guardrails against unsupported details.
Integration engineering: Resolved issues involving Stripe subscription events, plan mapping, and Supabase response handling.
End-to-end testing: Completed five testing rounds, identifying and fixing more than 25 bugs across the integrated system.
Cross-functional collaboration: Coordinated with the frontend engineer on database requirements and data contracts to connect backend workflows with the React dashboard.
Closing Summary
MeinBüromeister demonstrates how workflow automation, AI-powered information extraction, and integrated SaaS infrastructure can simplify construction reporting. The primary contribution was engineering the backend workflows and integrations that connect field communication with structured reports, subscription management, and the office dashboard.
Turning customer voice notes, photos and text messages on WhatsApp into instant, database-backed answers through an n8n workflow.
Section: n8n Automations, AI Agents for WhatsApp
Overview
This project is a WhatsApp business assistant for an online store. Customers write, speak or send a photo, and the workflow replies on WhatsApp with facts taken from the store’s Supabase product database, answering by voice when the customer used voice. It runs on n8n cloud with OpenAI models and replaces manual answering of repetitive product questions.
The business problem
Store owners answer the same questions all day: is this in stock, what does it cost, how long is delivery. Customers send these as voice notes, typed messages or screenshots of a product, so a simple keyword bot cannot read them. Slow replies lose sales, and a bot that guesses prices or stock damages trust.
The solution
An n8n workflow receives every WhatsApp message and routes it by type. Voice notes are transcribed, photos are described by a vision model, and text goes straight through. All three are normalized into one message and passed to an AI agent that must look up facts in Supabase before answering. The reply goes back as text, or as a voice note when the customer sent audio. Memory keyed on the customer’s phone number keeps the conversation connected.
Every message passes through six stages, from the customer’s WhatsApp message to the reply, in a single n8n workflow.
Step
Process
1. WhatsApp input
The customer sends a voice note, a text or a photo. The WhatsApp Trigger node receives it.
2. Routing
A Switch node checks the message type and sends it down the audio, text or image path.
3. Media handling
Voice notes and photos are downloaded through the WhatsApp media API. A voice note is transcribed and a photo is described by a vision model.
4. Normalizing
Edit Fields nodes turn each path into the same two fields: the message text and the customer’s phone number as session id.
5. AI agent
The agent looks up facts in the Supabase products table, thinks again, and writes a short reply. It remembers the conversation by phone number.
6. Reply
If the customer sent audio, the reply is converted to speech and sent as a voice note. Otherwise it is sent as text.
The workflow in n8n
Image: The top branch handles voice notes, the middle branch handles text, and the bottom branch handles photos. All three meet at the AI Agent, which has its model, memory, a Think tool and the Supabase Vector Store attached.
Supporting integrations
WhatsApp Business Cloud API: receives customer messages, downloads media and sends replies.
OpenAI: transcribes audio, describes images, runs the agent (GPT-4.1-mini) and creates the spoken reply.
Supabase: vector store over the products table, queried by the agent as a tool.
n8n Simple Memory: keeps recent messages per customer phone number.
Core Workflow Configuration
The workflow is built from a handful of node groups. Each one has a single job and passes the same two fields forward, so the agent never needs to know how the message arrived.
Node group
Configuration
WhatsApp Trigger
Listens for message updates from the WhatsApp Business Cloud API.
Switch
Three rules: audio (by mime type), text (by message type) and image (by mime type).
Download media + HTTP Request
Gets the media URL from WhatsApp, then downloads the file with the WhatsApp credential. Used for both audio and images.
Transcribe a recording
OpenAI audio transcription of the downloaded voice note.
Analyze image
OpenAI vision (GPT-4o-mini) on the downloaded photo, returned as a text description.
Edit Fields (three)
Set userMessage (the text, transcript or image description) and sessionId (the customer’s phone number).
AI Agent
GPT-4.1-mini with a system prompt: short WhatsApp-style replies under 80 words, one emoji at most, never guess, answer only from the database, hand complex issues to human support.
Simple Memory
Window memory keyed on the customer’s phone number.
Supabase Vector Store
Retrieve-as-tool on the products table, with OpenAI embeddings.
Think
A reasoning tool the agent can use before answering.
If
Checks whether the original message was audio.
Generate audio + Send voice reply
Turns the answer into speech and sends it back as an audio message.
Send message
Sends the answer as a WhatsApp text message.
Product Experience
These three mockups show how the assistant is meant to feel on the customer’s phone, one for each input type. They are illustrative designs, not screenshots of live conversations. The status messages (“Transcribing…”, “Searching our catalog…”) and the Shopify order lookup and replacement order shown in scenarios 1 and 3 are not in the current workflow and belong to a planned extension.
Scenario 1: voice input
Image: The customer sends a voice note. The workflow transcribes it, the agent finds the answer, and the reply comes back as a voice note because the customer used voice.
Scenario 2: text input
Image: A typed question goes straight to the agent, which checks the product data and answers in two or three short lines, with a follow-up question to move the sale forward.
Scenario 3: image input
Image: A photo is described by the vision model, and that description is handled as the customer’s message. In the current workflow the agent matches it against the product table. Order lookup and replacement orders are future work.
Results & Metrics
The workflow is built and cleaned up, and no live customer data has been measured yet. The figures below describe what was delivered. Response time, accuracy and conversion numbers should be added after a pilot with real conversations.
Metric
Result
Production workflows
1 main agent workflow
n8n nodes
21 after cleanup
Input types supported
3: voice note, text, photo
Reply formats
2: text and voice note
AI models used
4: transcription, vision, agent, speech
Knowledge source
Supabase products table, searched by the agent as a tool
Workflow defects fixed
2 (placeholder Code node, wrong message type)
Live response time and accuracy
Not measured yet
Technical Challenges & Solutions
Reviewing the exported workflow turned up problems that would have stopped it from working in production.
Voice reply could not be sent
Challenge
The voice path ended in a Code node that still held the placeholder YOUR_BASE64_STRING_HERE, and the send node used the document message type.
Solution
Removed the Code node, connected “Generate audio” directly to the send node, renamed it “Send voice reply”, and set its message type to audio with the customer’s number as recipient.
Outcome
The voice path is complete from the audio message to the voice reply.
Credentials missing after import
Challenge
After importing the workflow into a new n8n account, every WhatsApp, OpenAI and Supabase node showed a warning.
Solution
Reselect the credentials on each flagged node. For presentations, a demo copy with sample output pinned on the main-path nodes can be used.
Outcome
The nodes can be run green once credentials are set. A demo copy was prepared for the voice path.
AI answers must stay factual
Challenge
A sales assistant that invents prices or stock levels loses customer trust.
Solution
The system prompt requires the agent to retrieve facts from Supabase before answering, to ask when data is missing, and to hand complex issues to human support. Replies are capped at 80 words.
Outcome
Answers stay short and grounded in the product table.
Secrets inside workflow files
Challenge
A related Shopify workflow contains an access token written directly into its HTTP nodes.
Solution
Rotate the token and move it into an n8n credential before sharing or publishing that workflow.
Outcome
Open item, to be done by the store owner.
Key Takeaways & Future Development
Key takeaways
One agent for three input types: routing by message type and normalizing to the same two fields keeps the agent simple.
Grounded answers: forcing a database lookup before every reply is what keeps the assistant safe to put in front of customers.
Voice in, voice out: answering in the customer’s own format makes the assistant feel natural on WhatsApp.
Future development
Connect Shopify so the agent can check order status and create replacement orders, as shown in the mockups.
Add progress messages such as “Transcribing your voice message” while longer steps run.
Hand over to a human on WhatsApp, with the conversation passed to the support team.
Run a pilot with real customers and record response time, answer accuracy and sales conversion.
Move secrets such as the Shopify token into n8n credentials.
An end-to-end system for automated inbound lead scoring, enrichment, and real-time sales routing, powered by GPT-4.1 and n8n.
Section: Custom SaaS Platforms
Image: The lead form with a sample lead and the AI results card: score, HOT category, intent, budget, urgency, ICP match, summary and recommended action.
Most sales teams receive inbound leads from multiple sources (website contact forms, paid ad landing pages, partner referrals, and API integrations) and have no consistent, fast way to decide which ones deserve immediate attention.
The core problems the team was facing:
No consistent scoring methodology: qualification decisions depended on whoever happened to read the form first, leading to missed opportunities and wasted rep time
Hot leads were going cold: without instant routing, a prospect who said “ASAP, budget approved” could wait hours before a rep saw the submission
Company intelligence had to be looked up manually: reps spent time researching the company behind an email address before they could even decide whether to respond
No tiered escalation: there was no system to distinguish between a lead that needed a call in the next hour versus one that could go into a drip sequence
No unified lead record: repeat contacts from the same email were logged as separate entries, losing the interaction history
The goal was to build a fully automated intake-to-routing pipeline: AI scoring that runs in under two seconds, automatic company enrichment from the email domain, tiered alerts that reach the right person at the right time, and a central record of every lead with full analysis history.
What this system does
The system is composed of three interconnected layers: a lead intake API, a backend processing pipeline, and a real-time alerting engine, all orchestrated through n8n and a Node.js Express server.
Lead intake
Accepts inbound leads via a hosted landing page form, a direct REST API call, an embedded form webhook, or an n8n-triggered workflow
Validates required fields (name, email) and normalises all input before processing begins
Detects repeat submissions from the same email address: existing records are updated with the new interaction rather than duplicated
Supports a source field to track which channel each lead arrived from (website, paid ad, referral, n8n, etc.)
AI scoring engine
Each lead is analysed by GPT-4.1 across five dimensions: budget signals, urgency language, purchase intent, ICP (Ideal Customer Profile) match, and email quality
Returns a structured score from 0–100 and classifies the lead as HOT, WARM, or COLD
Produces a 12-field analysis object: score, category, intent, budget, urgency, ICP match, AI summary, recommended action, key signals, risk factors, estimated deal size, and sales rep assignment
Falls back to a rule-based keyword scoring engine if the OpenAI API is unavailable, guaranteeing zero-downtime operation
Enrichment layer
Extracts the company domain from the lead’s email address and enriches it with external company intelligence
With Clearbit: pulls company name, industry, employee count, estimated revenue, tech stack, LinkedIn URL, and headquarters location
Without Clearbit: performs basic domain analysis, infers company name, identifies TLD-based region, and confirms it is a business (not free) email
Free email providers (Gmail, Yahoo, Hotmail, etc.) are skipped for enrichment and flagged accordingly
Alerting and routing
Fires Slack alerts for every HOT lead: rich Block Kit messages, including all 12 analysis fields, AI summary, estimated deal size, and assigned sales team
Routes WARM leads to a nurture queue for email sequence follow-up
Archives COLD leads automatically with no sales action triggered
All notification steps run asynchronously, so they never delay the API response back to the form or caller
CRM integration
Pushes every processed lead to Airtable with 18 mapped fields, including all analysis outputs
Designed to be swapped for HubSpot or any other CRM with a single service file change
Skips gracefully if CRM credentials are not configured: the pipeline continues without interruption
Benefits
Hot leads reach a rep in seconds, not hours, the moment a high-intent submission arrives, a Slack alert lands in the right channel with everything the rep needs to act
Consistent, bias-free scoring: every lead is evaluated against the same GPT-4.1 criteria, removing the inconsistency of human triage
Company intelligence without manual research: reps open a Slack alert and already know the company’s industry, size, and tech stack
Zero-downtime resilience: if the AI API goes down, rule-based fallback scoring keeps the pipeline running
Unified lead history: repeat contacts are detected, and interactions are appended to the original record, preserving full context
Clean data from day one: lead sources are tracked, free email submissions are flagged, and all records share a consistent schema
Scalable without config changes: new lead sources plug in via the webhook endpoint; the pipeline handles volume automatically
How it works (simplified)
A lead arrives via the landing page form, a direct API call, an embedded form, or an n8n webhook, all routes converge on the same processing pipeline
The system checks for a repeat submission by looking up the email address in the lead store; if found, the new interaction is logged against the existing record
The lead is enriched: the email domain is used to pull company data from Clearbit, or a basic domain analysis runs as a fallback
GPT-4.1 analyses the lead: the full enriched record is submitted to the AI with a structured scoring prompt; a 12-field JSON response is returned in under two seconds
The lead is stored in the in-memory data layer (replaceable with PostgreSQL or Supabase) and a full analysis record is written
HOT lead → Slack alert is sent to the relevant sales channel, tagging the assigned rep team with the full AI summary and deal estimate
WARM lead → nurture queue: logged for drip email sequence follow-up
COLD lead → archived: stored for reference with no immediate sales action
The CRM is updated asynchronously: Airtable receives the full 18-field record without blocking the API response
The caller receives an instant response: the full analysis JSON is returned to the form or API caller within the same two-second window
System architecture (simplified)
Image: Architecture: four lead sources feed an Express backend with a five-step pipeline (validate, repeat detection, Clearbit enrichment, GPT-4.1 scoring, store), which routes to Slack alerts, an Airtable CRM and a nurture queue.
Ops-controlled settings (thresholds, routing rules, and CRM field mappings) are managed through environment configuration:
Setting
Where Configured
What It Controls
OPENAI_API_KEY
.env file
Enables AI scoring via GPT-4.1
SLACK_WEBHOOK_URL
.env file
Destination channel for HOT lead alerts
AIRTABLE_API_KEY + BASE_ID
.env file
CRM destination for all processed leads
CLEARBIT_API_KEY
.env file
Enables full company enrichment (optional)
Alert Toggle (per lead)
Google Sheets control layer
Enable or disable alerts per lead source
Score thresholds
Prompt config in aiService.js
Adjustable scoring bands per business context
Dashboard overview
Lead analysis results panel
The lead form renders a live results card immediately after submission, displaying the AI score, category, intent rating, budget signal, urgency level, ICP match, and a plain-English summary of why the lead scored the way it did, along with a specific recommended next action.
Renders in the browser within ~2 seconds of form submission
Slack alert panel
When a lead is classified as HOT, a Block Kit alert fires to the configured Slack channel. The message includes: lead name and company, email, phone, all five scoring dimensions, the AI-generated summary, recommended action, estimated deal size, and assigned sales team, formatted as a structured card with section dividers.
Delivered to the #sales-alerts channel within seconds of submission
Analytics dashboard
The /dashboard page provides a live operations view of all processed leads: total volume, HOT / WARM / COLD breakdown, average score, conversion rate, leads today, leads this week, top sources by volume, and a filterable lead feed. Data is drawn directly from the in-memory store and refreshes on page load.
Accessible at /dashboard: no login required in base configuration
Project evolution & key milestones
The system scope grew significantly from the initial lead scoring proof-of-concept. Key additions and refinements delivered across the project lifecycle:
Core scoring engine
Initial GPT-4 integration replaced with GPT-4.1 for faster, more consistent structured output
AI temperature set to 0.3 after testing: balances consistency with contextual sensitivity
Strict JSON output schema enforced via system prompt to eliminate parsing failures
Rule-based fallback engine added to ensure zero downtime when the OpenAI API is unavailable
Score validation and sanitisation layer added (score clamped 0–100; category constrained to enum)
Enrichment layer
Clearbit integration added as the primary enrichment source for business email domains
Domain-only fallback implemented: company name inference, TLD region mapping, business email detection
Free email provider list maintained to skip enrichment for Gmail, Yahoo, Hotmail, iCloud, ProtonMail, and similar
Alerting system
Slack alerts upgraded from plain text to full Block Kit format with eight labelled fields and section dividers
All notification steps moved to asynchronous execution to keep main API response time under two seconds
CRM push also made non-blocking: Airtable delays no longer affect the caller experience
Data and integration
n8n webhook route added (/api/webhook/n8n) to support visual workflow orchestration
Embedded form endpoint added (/api/webhook/form) for third-party form providers
Importable n8n workflow JSON file shipped with the project for instant setup
Airtable CRM integration added with 18 mapped fields; designed as a swappable service module
Memory layer and analytics
Repeat lead detection added: duplicate email submissions update the existing record rather than creating a new one
Interaction history log added to each lead record for full contact context
Analytics aggregation engine built into the storage service: computes category counts, average score, conversion rate, score distribution, top sources, and temporal metrics on demand
Dashboard feed endpoint added with configurable pagination and category/source filters
Security and production hardening
Helmet HTTP security headers added across all routes
Rate limiter set to 100 requests per 15 minutes per IP on all /api/* routes
Request body size capped at 1 MB to prevent oversized payload attacks
Explicit timeouts set on all external API calls (OpenAI: 30s · Clearbit: 5s · Airtable: 10s · Slack: 5s)
Global error handler middleware added to prevent stack traces leaking to API callers
Data flow architecture
How data moves through each stage and at what cadence, a key reference for ops teams integrating this system into existing stacks:
Stage
Source
Destination
Trigger
Intake
Form / API / n8n webhook
Express backend
On submission (real-time)
Enrichment
Clearbit API / domain analysis
Lead record (in memory)
Per lead, synchronous
AI Scoring
OpenAI GPT-4.1
Lead record (in memory)
Per lead, synchronous
CRM Push
In-memory lead record
Airtable (18 fields)
Per lead, async (non-blocking)
Slack Alert
In-memory lead record
Slack channel (HOT only)
Per lead, async (non-blocking)
Dashboard
In-memory lead store
Browser via /dashboard
On page load / manual refresh
Note
The current storage layer uses an in-memory Map for speed and simplicity. For production deployments requiring persistence across restarts or horizontal scaling, this is designed to be swapped for PostgreSQL, Supabase, or MongoDB, the storage service exposes a clean interface with no direct database calls in the route layer.
Lead scoring & classification
The AI scoring engine evaluates every lead across five weighted dimensions and assigns a 0–100 score. Classification thresholds are:
Category
Score Range
Routing Action
Criteria
HOT
80 – 100
Immediate Slack alert + rep assignment
Clear budget, urgency signal, decision-maker identified, strong ICP match
WARM
50 – 79
Added to nurture email sequence
Some buying signals, partial ICP match, needs further qualification
COLD
0 – 49
Archived, no immediate action
No clear intent, no budget signals, poor ICP fit or generic enquiry
The five scoring dimensions GPT-4.1 evaluates are:
Budget: explicit dollar amounts, investment language, company size inference
Automatically sorting a backlog of HubSpot companies into industry groups with Google Gemini, and flagging the ones that need a human.
Section: n8n Automations, AI Agents
Overview
This project is an automation that fills in the Industry Group field on HubSpot company records. An n8n workflow runs every hour, takes companies from a HubSpot list, skips the ones that already have an industry group, asks Google Gemini to pick one group for each, and writes the answer back to HubSpot together with a confidence score and a one-line reason. Companies it cannot classify with confidence are marked Needs R&D for a person to review instead of being forced into a wrong category.
The business problem
A CRM is only useful for segmentation and reporting when every company has an industry. Filling the field by hand for a long backlog of companies is slow, and different people choose different categories for the same kind of business. Letting an AI write straight into the CRM brings its own risks: an invented category, a low-confidence guess recorded as fact, or the same company processed over and over.
The solution
The workflow reads the allowed Industry Group options directly from HubSpot, so the AI can only choose values that exist. Each company is handled one at a time. Gemini returns a single group, a confidence score and a reason in JSON, and a code node checks that the answer matches a real HubSpot option and clears a confidence threshold before anything is written. Anything that fails that check is labelled Needs R&D. A dry-run switch lets the team test the whole flow without changing any record.
Project snapshot
Item
Detail
Platform
n8n
Trigger
Schedule, every hour
CRM
HubSpot (companies, a list of companies to process, the hs_industry_group property)
AI model
Google Gemini Flash, temperature 0.2, no web search
Batch size
Up to 100 companies per run, one at a time
Confidence threshold
0.6
Safety switch
dryRun setting, off by default
Workflow size
22 nodes
Status
Active
How the System Works
Each run follows the same six stages, from the schedule firing to a company updated in HubSpot.
Step
Process
1. Schedule and setup
The Schedule Trigger starts the run every hour. Two things happen in parallel: the workflow loads the allowed Industry Group options from HubSpot, and sets its configuration (companies per run, confidence threshold, dry-run flag and the HubSpot list id).
2. Collect the backlog
The workflow pages through the HubSpot list, 250 members at a time, until it has the configured maximum of 100 companies, then splits them into single items.
3. One company at a time
A loop takes each company and fetches its name, domain, website, additional domains and current industry group. Companies that already have an industry group are skipped.
4. Prepare and check
A code node cleans the domains. If a company has neither a domain nor a name, it is skipped because there is nothing to classify.
5. Gemini classification
Gemini receives the name and domain with the list of allowed groups and the classification rules, and returns JSON with the industry, a confidence score and a reason.
6. Validate and write
A code node matches the answer to a real HubSpot option and applies the confidence gate. Unless dry run is on, the result is written to HubSpot, followed by a Wait node before the next company.
The workflow in n8n
Image: The two chains on the left prepare the run: the top one loads the valid industries, and the bottom one pages through the HubSpot list. The loop in the middle processes one company at a time through the Gemini step.
Supporting integrations
HubSpot: the list of companies to process, the company records, the allowed Industry Group options and the update call.
Google Gemini: the classification model, with web search switched off so it answers from its own knowledge.
n8n workflow static data: holds the pages of list members while they are being collected.
Core Workflow Configuration
The workflow is built from a handful of node groups. Settings that a person may want to change are kept together in one Init Search Config node.
Node group
Configuration
Schedule Trigger
Runs every hour.
Get Industry Group Options / Extract Valid Industries
Reads the hs_industry_group property definition from HubSpot. Builds the list of allowed labels, leaving out hidden options and Needs R&D (which is added back in the prompt), and keeps the raw options for matching later. Retries up to 3 times, 5 seconds apart.
Init Search Config
Sets maxCompanies 100, confidenceThreshold 0.6, dryRun false and listId 638.
Get List Memberships / Accumulate List Members / Has More List Pages?
Pages through the HubSpot list 250 members at a time, stores them in workflow static data and stops at the company limit.
Collect List Members / Split Company IDs
Trims the collected members to the limit, clears the stored data and splits them into one item per company.
Loop Companies (1 at a time)
Processes one company per round. Its finished output goes to Backlog Run Complete, which records the completion time.
Get Company
Fetches name, domain, website, additional domains and current industry group from HubSpot. Retries up to 3 times.
IF Industry Group Missing
Continues only when the industry group is empty, so companies already classified are never touched again.
Clean Domain (code)
Strips protocol, www, paths and ports from the domain, website and additional domains, removes duplicates and builds a clean domain list.
IF Searchable
Continues only if the company has at least one domain or a name.
Gemini Classify Industry
Gemini Flash at temperature 0.2 with a detailed prompt: allowed groups, group definitions, classification rules and a JSON output format. Web search is off. Retries up to 5 times.
Match Industry Value (code)
Reads the JSON from the model, caps the confidence at 0.65 when there is no domain, and matches the chosen group to a real HubSpot option by label, value or a normalized form.
Prepare Company Update (code)
Applies the confidence gate and builds the HubSpot properties: the group (or Needs R&D), confidence, a reasoning text cleaned of stray characters and limited to 500 characters, a status and the check date.
IF Dry Run? / Dry Run - Skip Update
In dry run mode the update is skipped and the loop continues.
Update Company Industry Group
PATCH request to the HubSpot company record. Retries up to 3 times.
Wait1
Pauses before the next company to stay within HubSpot rate limits.
Classification Rules and Scenarios
What the model is told
Gemini must choose exactly one group from the list HubSpot provides. The prompt defines each group (for example Manufacturing, eCommerce, Retail, Wholesale Distribution, SaaS, Development shop, IT / MSP, Low Voltage, Consulting, Finance, 3PL, Transportation, Energy Rebates and Self Storage) and sets rules for the edge cases:
Businesses that sell physical products to consumers are never excluded. They go to Retail (walk-in stores) or eCommerce (online-first and direct-to-consumer brands).
Local consumer services, restaurants, medical and dental practices, schools, religious organizations and home services are marked Needs R&D with full confidence and an excluded flag.
A company that cannot be identified, or fits none of the groups (such as a law firm or nonprofit), is marked Needs R&D instead of being forced into a group.
Confidence is 0.80 to 1.0 when the company is clearly known, 0.40 to 0.65 for partial evidence or a name with no domain, and 0.0 when nothing can be verified.
The answer must be a single JSON object with industry, confidence, excluded and reasoning.
Scenarios
These scenarios show how the workflow treats the main kinds of company. They are illustrative, derived from the prompt and code, and are not recordings of real HubSpot records.
Scenario
What arrives
What the workflow does
Result in HubSpot
1. Clearly known business
A company with a domain whose business model the model recognizes, for example a software platform.
Gemini returns the matching group with confidence 0.80 or higher. The code matches it to a HubSpot option and it passes the 0.6 gate.
Industry group set, status updated, confidence and reason saved.
2. Consumer brand
A clothing or electronics brand selling to the public.
Rule 3 sends it to Retail or eCommerce, never to Needs R&D.
Retail or eCommerce group set.
3. Local consumer service
A pizzeria, dental practice or salon.
Excluded under rule 4 with confidence 1.0.
Group set to Needs R&D for a human to see.
4. Unknown company
A company the model cannot identify.
Returns Needs R&D with low confidence, so the gate fails.
Group set to Needs R&D, status low_confidence or unmatched.
5. Name only
A company with a name but no domain.
Confidence is capped at 0.65 in code, so the model cannot be fully sure.
Written only if it still clears 0.6, otherwise Needs R&D with status no_domain.
6. Already classified
A company that already has an industry group.
Stopped by IF Industry Group Missing, no model call.
Unchanged.
7. Dry run
Any company, with dryRun set to true.
Everything runs except the HubSpot update.
Nothing written.
Results & Metrics
The workflow is built and marked active. No run results were provided, so classification accuracy, the share of companies sent to Needs R&D and the time saved have not been measured.
Metric
Result
Production workflows
1
n8n nodes
22
Trigger
Schedule, every hour
Companies per run
Up to 100, processed one at a time
Industry groups
Read live from HubSpot, so the model cannot pick a value that does not exist
Confidence gate
0.6, with a 0.65 cap when a company has no domain
Fields written per company
Industry group, confidence, reasoning, status and check date
Safety features
Dry-run switch, skip of already classified companies, Needs R&D fallback, retries on every network and model call
Accuracy, volume, time saved
Not measured yet
Technical Challenges & Solutions
The model could invent a category
Challenge
A free-text answer such as “Software” or “Tech services” does not match any HubSpot option and would break the update or pollute the field.
Solution
The allowed groups are loaded from HubSpot on every run and given to the model to copy exactly. The answer is then matched to a real option by label, value or a normalized form that ignores case, punctuation and “&” versus “and”.
Outcome
Only values that exist in HubSpot can be written.
A weak guess recorded as fact
Challenge
A model will always give an answer, even when it knows nothing about the company.
Solution
The model must return a confidence score, name-only companies are capped at 0.65, and a code node only writes a group when the answer matches an option and reaches 0.6. Everything else becomes Needs R&D, and the confidence, reason, status and check date are saved on the record.
Outcome
Doubtful cases are visible to a person instead of hidden among correct ones.
Processing the same companies again
Challenge
An hourly schedule over the same list would classify the same companies on every run and waste model calls.
Solution
The workflow only continues when the industry group is empty, so each company is processed once.
Outcome
Each run moves on to the next unclassified companies.
Reading a long list from HubSpot
Challenge
The HubSpot list API returns 250 members per page, and the run must stop at a fixed number of companies.
Solution
A loop collects pages in workflow static data until the next page is missing or the limit is reached, then clears the data and splits the members into single items.
Outcome
Runs are bounded and predictable.
Testing without touching real data
Challenge
Changing the prompt or rules on live CRM records is risky.
Solution
A dryRun setting runs the whole flow, including the model call, but skips the HubSpot update.
Outcome
The prompt can be tuned and checked before records are changed.
Open items found in the export
Challenge
Three details in the workflow need checking. The Wait node after each update has an amount of 10 but no unit stored, and n8n’s default unit for that node is hours. The prompt refers to a Seamless Data field that no earlier node fills, so it is always “N/A”. The collected list members sit in static data, so a run that fails halfway through paging could leave old members behind for the next run.
Solution
Open the Wait node and set the unit to seconds. Either feed the Seamless description into the prompt or remove the line. Clear the stored list at the start of each run.
Outcome
Not done yet. These are the first improvements to make.
Key Takeaways & Future Development
Key takeaways
Let the CRM define the answers: loading the allowed groups from HubSpot keeps the model inside the structure the team already uses.
Check the model’s output in code: a confidence gate and a strict match to real options decide what is written, not the model alone.
Send doubt to a person: Needs R&D with a stored reason turns uncertain cases into a review list.
Make it safe to test: the dry-run switch, retries and the skip of classified companies make the workflow safe to leave running.
Future development
Set the Wait node unit to seconds and check that the Gemini model id is available in your account.
Fill the Seamless Data line in the prompt or remove it.
Clear the stored list members at the start of every run.
Create a HubSpot view of companies marked Needs R&D, sorted by confidence, so reviewers can work through them.
Send a short summary after each run (companies processed, written, flagged) using the Backlog Run Complete step.
Run a measured pilot, compare a sample of results with a person’s choices, and use it to complete the results section.
How Harrington & Cole LLP replaced four disconnected tools with one Airtable workspace, cutting matter-intake time by 68% and automating 1,200+ status touchpoints a month.
Harrington & Cole LLP is a 60-person law firm headquartered in Halifax, Nova Scotia, with three practice groups and a docket of roughly 230 active matters at any given time. By early 2025 the firm was running case intake, conflict checks, scheduling and time capture across four disconnected systems: a legacy practice-management tool, shared Excel files on a network drive, Outlook calendars, and a paper intake form. Status updates lived in attorneys’ heads and email threads.
Over an 11-week engagement we replaced this patchwork with a single Airtable workspace, purpose-built tables, role-specific Interfaces for attorneys and paralegals, and an automation layer that handles the firm’s repetitive coordination work. The implementation was delivered without disruption to billable work and went live with all 60 users on the same day.
Outcomes after 90 days in production
68%: Faster matter intake
1,284: Automated touchpoints / mo.
0: Missed SOL deadlines
$41K: Recovered WIP in Q1
At a glance
Before: 4 systems, ~6 hrs to open a new matter, manual conflict checks, scheduling done by email.
After: 1 Airtable workspace, ~2 hrs to open a matter end-to-end, conflict-check workflow runs in < 60 seconds, hearing prep tasks auto-spawn 14 days out.
Background
The client and the problem
About the firm
Harrington & Cole LLP serves regional commercial clients across Atlantic Canada. Three practice groups (litigation, corporate / transactional, and employment) share a managing partner, a central paralegal pool, and a single billing department. The firm had grown roughly 40% in headcount over five years; its tooling had not kept up.
The pain points that triggered the project
Matter intake took up to six hours. A new lead required duplicate data entry across four systems, with manual conflict checks against an Excel register that one paralegal owned.
Status was invisible. Partners had no reliable view of which matters were stuck in discovery, which were close to trial, or which had open balances over 60 days.
Deadline slips were happening. Two near-misses on statute-of-limitations dates in 2024 prompted the managing partner to require automated alerting as a non-negotiable.
Time capture was lossy. Attorneys logged time at week-end from memory; the firm estimated 6–9% of billable hours were being lost.
Reporting was a fire drill. The monthly management report took the office manager roughly two days to assemble from exports.
Why Airtable
The firm evaluated three traditional practice-management products and Airtable. The decision came down to three factors: (1) the firm wanted a system its own staff could change without a vendor ticket, (2) the data model needed to fit the firm’s terminology rather than force the firm into a vendor’s, and (3) the integration story with Slack, Outlook and DocuSign needed to be straightforward. Airtable’s linked-record model, Interface Designer, and native automation framework made it the right fit.
Approach
How the implementation was run
We delivered the implementation in four phases over eleven weeks. A core working group made up of the managing partner, two senior associates (one per practice area), the office manager, and a paralegal lead met twice a week to validate the data model, review prototypes, and sign off on go-live readiness.
Phase
Weeks
What we did
Output
1. Discovery
1–2
Shadowed intake, conflict-check and billing workflows. Mapped 14 existing data sources. Interviewed 11 attorneys and staff.
Process map, data dictionary, ranked pain-point list.
2. Data model & schema
3–4
Designed 10-table relational schema. Built linked records for Cases ↔ Clients ↔ Hearings ↔ Documents ↔ Tasks ↔ Time entries.
Live Airtable base with sample data; ER diagram.
3. Interfaces & automations
5–8
Built the Attorney Dashboard, the Paralegal Workbench and the Partner Reporting interfaces. Designed and tested 8 automations.
3 production interfaces; automation library.
4. Migration, training, go-live
9–11
Migrated 1,847 historical matters, trained all 60 users in three cohorts, ran a 5-day parallel-run before cutover.
Single-day cutover with zero data loss.
Guiding principles
Match the firm’s vocabulary. Field names mirror how attorneys actually speak (e.g. matter, not project; SOL date, not deadline).
One source of truth. Every piece of data lives in exactly one table; everywhere else uses a linked record, lookup or rollup.
Make the right thing the easy thing. Intake, conflict checks, and time capture were redesigned so the compliant workflow is the fastest workflow.
Internal ownership. The office manager and one senior paralegal were trained as base administrators; they own day-to-day changes.
What Was Built
The Cases table: the heart of the workspace
Every matter the firm works on is a record in the Cases table. From here, attorneys can see the entire docket grouped by practice area, filtered to their own matters, or sliced by stage. The grid below shows the production view used by senior associates each morning to triage their day.
Image: Figure 1: The Cases grid view, grouped by practice area. Coloured stage chips give partners an at-a-glance read on where every matter sits. The footer rolls up open balances and average days-open across the visible records.
Highlights of this view
Grouping by practice area mirrors how the managing partner thinks about workload distribution.
Stage is a single-select with a controlled vocabulary (Intake → Pleadings → Discovery → Negotiation → Trial Prep → Closed). It drives both the kanban view and three automations.
Lead Attorney links to the Staff table, so renaming or reassigning happens in one place.
Open Balance at the bottom is a rollup from the Time entries table, no manual reconciliation needed.
Two more views the firm uses every day
Kanban by stage is how partners review pipeline in their Monday morning meeting. Dragging a card moves the matter through the lifecycle and triggers the corresponding automation chain.
Image: Figure 2: Kanban view of the Cases table, grouped by stage. Card colour and the avatar in the corner make ownership obvious even on a wall display.
The expanded matter record
Clicking any row opens a full-detail view of the matter. This is the single screen attorneys live in when they’re working a case. It consolidates parties, deadlines, team, financials and notes. WIP and Open Balance are rollups from the linked Time entries table; the Statute of Limitations warning is a formula that turns red automatically inside 90 days.
Image: Figure 3: The expanded record view for matter CASE-2024-0142. Rollups, formulas and linked records mean every number on this screen is computed live from the underlying tables: nothing is manually maintained.
The Data Model
Ten tables, one source of truth
The base is built around Cases as the central entity. Everything else (clients, hearings, documents, tasks, time, communications) links to a case. This single design decision is what makes the rollups, formulas and automations possible.
Image: Figure 4: The 10-table schema. Lines indicate linked-record relationships. Cases is the hub; Documents, Tasks, Time entries and Communications all link to a case.
Field-level decisions worth calling out
Stage is a single-select with synced colour. Changes here trigger 3 different automations.
SOL date is paired with a formula days_until_sol; the alert automation reads the formula field, so all date logic lives in one place.
Conflict_clear on Clients is a checkbox controlled by the conflict-check automation, an intake matter is blocked from advancing until this is true.
Time entries rolls up into Cases as WIP and Open Balance, so billing reports build themselves.
Communications is populated by the Gmail and Outlook integrations, every email about a matter is logged automatically.
Interfaces
Three role-based interfaces, one underlying base
Different people in the firm need different things from the same data. We built three Interfaces on top of the base, each tailored to a role. All three read from and write to the same tables. There is no duplication.
The Attorney Dashboard
This is the screen attorneys open when they sit down at their desk. It surfaces the four KPIs that matter to them, the next 30 days of hearings, and their personal task list, all filtered to their own user automatically.
Image: Figure 5: The Attorney Dashboard interface, scoped to the logged-in user. KPI cards on top; upcoming hearings and personal tasks below. Click-through on any matter title opens the full case record in a side panel.
Two other interfaces
Paralegal Workbench: task-centric view. Paralegals can claim open tasks, log time directly against a matter, and upload signed documents which auto-update the matter’s stage.
Partner Reporting: read-only firm-wide view with charts showing matters by stage, WIP trend, top 10 oldest open balances, and attorney utilisation. Replaces what used to be a 2-day monthly reporting exercise.
Automations
Automating the repetitive coordination work
Eight automations remove roughly 1,200 manual touchpoints from the firm’s month. None of them replace an attorney’s judgment: they remove the surrounding paperwork.
Image: Figure 6: The Automations panel. Eight automations, six currently enabled, with run counts visible per automation. The detail pane on the right shows the trigger and three actions of the hearing-reminder workflow.
The automation catalogue
Each automation is owned by a named person on the working group, documented in a Notion runbook, and reviewed quarterly.
#
Automation
Trigger
Actions
Replaces
1
New case → Conflict check workflow
Record created in Cases with stage = Intake
Searches Clients table for matches against parties, opposing counsel and judges. Posts result to Slack and sets conflict_clear if no match.
Manual lookup in Excel register
2
Hearing date approaching (14 d)
days_until formula on Hearings = 14 and status = Scheduled
Slack DM to lead attorney + creates 4 prep tasks (brief, exhibits, witness prep, filings).
Calendar reminders + paralegal phone calls
3
Statute of limitations alert (90/30/7 days)
Cases.days_until_sol formula crosses each threshold
Email to lead attorney; at 30d also CCs managing partner; at 7d escalates to a Slack channel.
Slack DM to managing partner with link to the entry.
End-of-month review (often too late)
6
Client onboarding sequence
Clients.conflict_clear changes to true
Sends client engagement letter via DocuSign, creates Drive folder with templates, books an intake call.
Three separate manual checklists
7
Weekly WIP digest
Schedule: every Monday 7:00 AM
Builds per-attorney summary of WIP, billed and aged AR; emails each lead attorney their digest.
Office manager spending Friday building reports
8
Stale matter (no activity 30 d)
Schedule: nightly scan
Sets a flag field; visible on the Partner Reporting interface for follow-up.
Nothing: these matters used to disappear
Worked example: automation #2 in detail
Trigger: a record in Hearings where the days_until formula evaluates to exactly 14 and status is Scheduled.
Action 1 (Find records): looks up the parent Case via the linked-record field to retrieve the lead attorney and client.
Action 2 (Send Slack): DMs the lead attorney with a deep link back to the case record and a one-line summary of the hearing.
Action 3 (Create tasks): creates four pre-templated prep tasks in the Tasks table, all linked to the same matter, with sensible default due dates working back from the hearing.
Replaced: three different paralegals each maintaining their own version of a hearing-prep checklist, and the senior associate manually pinging attorneys.
Integrations
How the workspace connects to the rest of the stack
Airtable doesn’t replace email, calendars, document storage or e-signature, and shouldn’t. It coordinates them.
System
Direction
What flows
Mechanism
Outlook / Microsoft 365
Two-way
Hearings sync to attorney calendars; calendar invites for new hearings auto-create Hearing records.
Microsoft Graph + Airtable sync
Gmail (firm address)
One-way (in)
Email referencing a matter ID is logged to the Communications table with a link back to the thread.
Native Airtable Gmail integration
DocuSign
Two-way
Engagement letters dispatched from Airtable; signed status triggers automation #4.
Webhooks via Airtable Automations
Slack
One-way (out)
Hearing reminders, SOL alerts, time-entry alerts, weekly WIP digests.
Native Slack action in Airtable Automations
Google Drive
One-way (out)
On client onboarding, a structured matter folder is created with the firm’s standard subfolder template.
Drive API call from Airtable script action
Notion
Reference only
Runbooks for each automation live in Notion; each Automation record has a URL field linking to its runbook.
Manual link
Permissions & data handling
Workspace plan: Airtable Business with SAML SSO via the firm’s identity provider. Two-factor authentication enforced for all 60 users.
Field-level permissions: rate cards, WIP and AR fields are read-only to associates; visible only to partners on the Partner Reporting interface.
Privileged matters: a checkbox on Cases hides a matter from all users not on its team via a row-level filter applied to every view.
Audit: Airtable’s revision history is retained, and a nightly snapshot is exported to the firm’s encrypted backup bucket.
Results
Measurable change after 90 days in production
Metric
Before
After
Change
Matter intake (lead → opened)
≈ 6 hours
≈ 1 hr 55 min
−68%
Conflict check turnaround
Up to 48 hr
< 60 sec (automated)
−99%
Missed SOL deadlines (rolling 12 mo.)
2 near-misses
0
−100%
Monthly management report build time
≈ 14 hours
≈ 25 min (live dashboard)
−97%
Time-entry capture latency
Avg. 4.2 days
Avg. 0.6 days
−86%
WIP recovered Q1 (vs. prior quarter)
n/a
$41,200
+ revenue
Automated touchpoints / month
0
1,284 (across 6 active flows)
n/a
Active users (firm-wide)
n/a
60 / 60
100% adoption
In the words of the firm
The thing that surprised me wasn’t the dashboards: it was the fact that we don’t talk about case status in hallways anymore. Everyone is looking at the same picture, and the system tells us when something needs attention before it becomes a fire. We have not had a near-miss on a deadline since the day we went live.
What we’d do again, and what we’d change
Worked well: embedding the working group from week one. Every design decision had a real attorney’s name attached to it, which made go-live painless.
Worked well: shipping the Attorney Dashboard before the Partner Reporting interface. Adoption is driven by the people doing the work, not the people watching the work.
Would change: we waited until phase 3 to wire up the Outlook two-way sync. In hindsight, that should have been in phase 2, calendars are how attorneys actually work.
Would change: add a fifth interface for the firm’s receptionist. Inbound caller intake is still slightly clunky.
Turning every email sent to a shared support inbox into a correctly routed, duplicate-free ClickUp ticket through one n8n workflow.
Section: n8n Automations, ClickUp Setups
Overview
This project is a support-intake automation for RefureTech. Customers email the shared support address, and the n8n workflow reads each message, works out which client it belongs to from the sender’s email domain, and files it in ClickUp. It becomes a new ticket in that client’s support list, a ticket in a triage list when the client is unknown, or a comment when a ticket for the conversation already exists. Attachments and earlier messages in the thread travel with it.
The business problem
Support requests arrive as ordinary email. Copying them into a ticket system by hand is slow and easy to get wrong: the wrong client, a missed reply, an attachment left behind in the inbox. The work grows with every new client. Automating it is not simple either, because ClickUp has its own Email ClickApp that sends a confirmation to the customer and files later replies as comments. A naive automation creates duplicate tickets or, worse, turns its own confirmation emails into new tickets in an endless loop.
The solution
A single n8n workflow polls the support inbox every minute. It looks up the client list stored in ClickUp, matches the sender by domain, checks four different ways whether the conversation already has a ticket or is already managed by ClickUp, and only then creates a ticket or comment. It uses plain, explainable rules in code nodes rather than an AI model, so every decision can be traced to a header, a subject line or a ClickUp field. Wherever a step could fail, the workflow prefers a duplicate ticket over a lost customer email.
Project snapshot
Image: Project snapshot.
How the System Works
Every email passes through six stages, from arrival in the inbox to a ticket in ClickUp, inside one workflow.
Step
Process
1. Email input
The Gmail Trigger polls the INBOX label every minute. The full message is fetched and its HTML body is converted to Markdown so it reads cleanly inside a ticket.
2. Client matching
The workflow reads the client list from ClickUp, where each client has a Domain, a folder id and a support list id. A code node picks the customer contact from the To, Cc and From addresses and matches its domain (or a parent domain) to a client. Free-mail and internal domains are never mapped.
3. Duplicate checks
Three lookups run: a ClickUp search by Gmail thread id, a ClickUp search by the thread ClickUp’s own confirmation email started, and a fetch of the full Gmail thread history.
4. Decision
Route Decision labels the email new, default, comment or skip. Two guard nodes can still turn a write into a skip: one if ClickUp’s ticket id is at the end of the subject, one if the mail headers show a machine-generated reply.
5. Ticket action
A Switch creates a ticket in the client’s list, creates a ticket in the triage list, or adds a comment to the existing ticket. The description holds the new message plus earlier messages in the conversation.
6. Attachments, label, sync
Attachments are downloaded and uploaded to the ticket, and the Gmail message is labelled as processed. For new tickets the workflow waits two minutes, finds ClickUp’s confirmation email and stores its thread id on the ticket so later replies are recognised.
The workflow in n8n
Image: The chain on the left reads the email, matches the client and checks for duplicates. The Switch in the middle sends each email to one of three ticket actions. Each action ends in its own attachment branch and a Gmail label step, and new tickets also pass through the wait and confirmation sync.
Supporting integrations
Gmail: trigger, message fetch with attachments, thread history, and labelling processed messages.
ClickUp: client routing data, ticket and comment creation, attachment upload, and custom fields that store the Gmail thread id and ClickUp’s email thread id.
Error workflow: a separate n8n workflow is attached in the settings to handle failures of this one.
Core Workflow Configuration
The workflow is built from a handful of node groups. Each has a single job, and the routing decision is carried forward as one item holding the same fields (contact, domain, folder, list, thread id, reason, action), so the ticket nodes never need to know how the decision was reached.
Node group
Configuration
Gmail Trigger
Polls every minute, restricted to the INBOX label on purpose. ClickUp sends its confirmation as the support address, which leaves a copy in Sent. Polling Sent would turn that confirmation into a ticket and trigger another confirmation, forever.
Get HTML + Markdown
Fetches the full message and converts the HTML body to Markdown. Retries up to 3 times, 2 seconds apart.
Get Support ID
ClickUp node that lists the client tasks. Each carries the custom fields Domain, folder_id and support_list_id.
Find ID from Domain (code)
Builds a domain-to-client map, ignores free-mail and internal domains, records domain collisions, picks the customer contact (mapped client first, then any external address, then the sender), validates every address, fills up to 20 Cc fields and applies the one hard skip: the self-addressed ClickUp acknowledgment.
Find Existing Ticket / Find ClickUp Email Thread
Two ClickUp API searches by custom field: the Gmail thread id, and the thread id of ClickUp’s own confirmation. If either call fails the workflow continues with no result, so a ticket is still created.
Get Thread History
Gmail API call for every message in the conversation. Failure is non-fatal: the ticket is created with the newest email only.
Route Decision (code)
Decides clickup_synced, comment, new or default, and assembles the ticket description with the earlier messages, capped at 60,000 characters.
ClickUp Already Owns This? (code)
Skips when the subject ends with a ClickUp ticket id such as #RTS-1234, or the confirmation wording is found. Anchored to the end of the subject so a customer’s own mid-subject reference such as #PO-88231 still gets a ticket.
Machine Auto-Reply? (code)
Skips out-of-office and bounce mail by reading headers (Auto-Submitted, X-Auto-Response-Suppress, Precedence, empty Return-Path), never the subject wording.
Switch
Six rules: new, default, comment, loop guard (skip), ClickUp synced (skip), auto-reply (skip). The fallback output goes to the triage list so an unexpected value cannot drop an email.
Create a task / Create Default Task
ClickUp nodes that create the ticket with the Markdown description, customer contact, recipient, up to 20 Cc contacts and the Gmail thread id as custom fields. Tickets opened mid-chain are titled with an [Ongoing thread] prefix.
Add Comment to Existing Task
Posts the new email as a comment on the tracked ticket without notifying everyone.
Attachment branches (3 sets)
Re-fetch the message with attachments, split them out, keep only files with a real file extension, upload them to the ticket and add the Gmail label. Emails without attachments still reach the label step.
Wait + confirmation sync
Waits 2 minutes, re-reads the task for its custom id, searches Gmail for ClickUp’s confirmation and saves its thread id into the ticket so future replies are recognised.
Product Experience and Routing Scenarios
Every email the workflow has handled is marked with a Ticketed label in Gmail, so the team can see at a glance which messages already have a ticket or comment in ClickUp. Replies on the same conversation carry the label too.
Routing scenarios
These scenarios show how the workflow treats the main kinds of email. They are illustrative, derived from the workflow logic, and are not recordings of real customer emails.
Scenario
What arrives
What the workflow does
Result
1. Mapped client, new request
A first email from someone at a client whose domain is in the ClickUp client list.
Matches the domain, finds no existing ticket, routes to the client’s folder and support list, uploads attachments, labels the email, then syncs ClickUp’s confirmation thread id after 2 minutes.
New ticket in the client’s support list.
2. Unknown domain
An email from a domain that is not mapped, or with no usable contact address.
Falls to the default branch. The ticket is still created, in the default triage list, with the sender as contact.
New ticket in the triage list for a person to assign.
3. Reply on a tracked conversation
A follow-up where a ticket already exists for the Gmail thread but ClickUp does not own the thread.
Finds the ticket by the Gmail thread id custom field and posts the email as a comment, then handles any attachments.
Comment and files added to the existing ticket.
4a. ClickUp-owned reply
A customer reply whose subject ends with a ClickUp ticket id.
Stops without writing. ClickUp’s Email ClickApp is already filing replies as comments, so acting would duplicate the ticket.
Skipped, no duplicate.
4b. Out-of-office or bounce
A vacation responder or delivery failure notice.
Detected from mail headers and skipped. A colleague’s real reply on the same thread is not skipped, because headers are checked instead of subject words.
Skipped, real replies kept.
4c. Our own confirmation
The ClickUp acknowledgment, sent from the support address back to the support address.
Hard skip by the loop guard.
Skipped, loop prevented.
Results & Metrics
The workflow is live and has run on the real support inbox. Business figures such as hours saved, tickets per week and routing accuracy have not been measured yet.
Metric
Result
Production workflows
1 main workflow, plus an error workflow attached in settings
n8n nodes
38
Trigger
Gmail polling every minute, INBOX only
Routing outcomes
6: new, default, comment, and three skip outcomes
Duplicate guards
4 layers: Gmail thread id, ClickUp email thread id, ticket id in subject, confirmation wording
Failure handling
Retries up to 3 times, 2 seconds apart, on every network node. Lookups fail open, so an email becomes a ticket rather than being lost.
9 runs took about 2 minutes (the path with the built-in 2-minute wait for ClickUp’s confirmation). 2 runs took about 4 seconds (paths without that wait).
Hours saved, accuracy, duplicate rate
Not measured yet
The execution count reflects only the executions n8n still retains and is not a full volume figure. The link between run time and path is inferred from the workflow design.
Technical Challenges & Solutions
Most of the engineering effort went into edge cases around ClickUp’s own email feature and around real-world email, where a simple “new email creates ticket” flow breaks quickly.
The workflow could answer itself forever
Challenge
ClickUp sends its confirmation email from the support address, so a copy lands in Sent. If the workflow read Sent, that confirmation would become a ticket, which triggers another confirmation, and so on.
Solution
The trigger watches INBOX only, and a loop guard in code skips any email sent from the support address with the support address in To or Cc. It is the only unconditional skip.
Outcome
The loop is closed twice, at the trigger and in the logic.
Duplicate tickets next to ClickUp’s Email ClickApp
Challenge
Once a ticket exists, ClickUp already files replies as comments and sends its own acknowledgment. Creating another ticket from the same reply duplicates work and makes ClickUp send the customer a second acknowledgment.
Solution
Four independent checks decide whether ClickUp owns the conversation: a stored ClickUp email thread id, the ticket id at the end of the subject, the confirmation wording in the thread, and the Gmail thread id on an existing ticket. The subject check is the most dependable, because older tickets lack the stored thread id and the confirmation wording can change.
Outcome
Replies on ClickUp-managed conversations are skipped and n8n stays out of the way.
Out-of-office mail versus a colleague covering for someone
Challenge
An auto-reply sets the subject to “Out of Office Re: …”. A colleague who replies on the same thread keeps that subject, so matching subject words would silently drop a genuine customer email.
Solution
The check reads mail headers that no human mail client sets (Auto-Submitted, X-Auto-Response-Suppress, Precedence, empty Return-Path). If the headers cannot be matched confidently to the right message, nothing is skipped.
Outcome
Machine mail is filtered out, and real replies with the same subject still become tickets.
A duplicate is acceptable, a lost email is not
Challenge
Lookups against ClickUp and Gmail can fail, and an email that stops the workflow is a customer who never gets answered.
Solution
The ticket-search and thread-history calls retry three times and then continue with no result. Missing results default to creating a ticket, the Switch fallback sends unknown outcomes to the triage list, and the split-out steps continue on error so every email still reaches the labelling step.
Outcome
Failures become an extra ticket or less context, never a dropped message.
Tickets opened in the middle of a conversation
Challenge
When the first email the workflow sees is already a reply, a ticket with only the latest message has no background.
Solution
The full Gmail thread is fetched, earlier messages are sorted by date and appended to the ticket description (with sender, recipient, date and subject), capped at 60,000 characters, and the title gets an [Ongoing thread] prefix.
Outcome
A person opening the ticket sees the whole conversation.
Malformed addresses breaking ticket creation
Challenge
ClickUp rejects the whole task when an email-typed field receives something that is not an address, such as a group name or undisclosed-recipients. Messy Cc lines and display names are common.
Solution
Header values are normalised, every address is validated, the support address is never mapped into contact fields, and Cc contacts are capped at 20 fields with the overflow count recorded.
Outcome
Odd headers no longer cause ticket creation to fail.
Secrets inside the workflow (open item)
Challenge
The ClickUp API token is written directly into the Authorization header of 8 HTTP Request nodes instead of an n8n credential, so it is visible to anyone who can open or export the workflow.
Solution
Create a ClickUp credential in n8n, switch those nodes to it, and rotate the token.
Outcome
Not done yet. The token is deliberately not reproduced in this document.
Key Takeaways & Future Development
Key takeaways
Design for the tool next door: the hardest part was coexisting with ClickUp’s own email automation, not creating tickets. Layered duplicate checks solved it.
Headers over words: deciding on machine-readable signals, such as mail headers and a ticket id anchored to the end of the subject, keeps genuine emails from being filtered out.
Fail open: choosing a duplicate over a lost email at every failure point makes the automation safe to leave running on a customer-facing inbox.
Configuration in ClickUp, not in code: adding a client means filling in three fields on a ClickUp task, with no change to the workflow.
Future development
Move the ClickUp token into an n8n credential and rotate it.
Log every skipped email (loop guard, ClickUp-owned, auto-reply) to a sheet or channel so skips can be audited.
Surface the domain collisions the code already detects, for example with an alert, so two clients cannot silently share a domain.
Replace the fixed 2-minute wait with a short retry loop that looks for ClickUp’s confirmation email.
Merge the three near-identical attachment branches into one shared sub-workflow.
Run a measured pilot and record tickets per week, time saved, routing accuracy and duplicate rate to complete the results section.
How Loomstack Inc. consolidated its customer-success stack into a single Airtable workspace, surfacing at-risk accounts 47 days earlier and lifting net revenue retention from 102% to 114%.
Loomstack Inc. is a Series B B2B SaaS company providing workflow automation tooling to mid-market and enterprise customers, with roughly $24M ARR across 280 paying accounts. By Q4 2025 the customer success team was operating across five disconnected tools: HubSpot for CRM data, Zendesk for support tickets, Stripe for subscriptions, a homegrown product-usage dashboard, and a sprawl of Google Sheets for everything that didn’t fit elsewhere. Health scoring was a quarterly spreadsheet exercise, and renewal forecasts were assembled by hand the week before each board meeting.
Over a 9-week engagement we replaced this stack’s coordination layer with a single Airtable workspace, a unified accounts table, computed health scores updated nightly, role-specific Interfaces for CSMs and the VP, and an automation suite that triggers playbooks before churn risk turns into a churn ticket. HubSpot, Zendesk and Stripe still own their primary data; Airtable is the operating surface on top.
Outcomes after 90 days in production
114%: Net revenue retention
47 d: Earlier risk detection
3,742: Automated runs / mo.
$1.2M: Saved ARR (Q1)
At a glance
Before: 5 disconnected tools, quarterly health scoring done in a spreadsheet, renewal forecast assembled by hand, average risk detection lag of 51 days.
After: 1 Airtable workspace as the CS operating layer, health scores recomputed nightly, live forecast on the VP dashboard, average risk detection lag of 4 days.
Background
The client and the problem
About Loomstack
Loomstack sells a workflow automation platform to mid-market and enterprise operations teams across North America and EMEA. Headcount is 180; the customer-facing org is split into 14 CSMs (segmented by Enterprise / Mid-Market), 6 support engineers, and 4 account executives covering expansion. The CS leadership reports to a VP of Customer Success who in turn reports to the COO.
The pain points that triggered the project
Churn was being detected too late. The first signal an account was in trouble was usually a renewal coming up in 30 days and a CSM scrambling. Two enterprise logos worth $640K combined churned in Q3 2025 with effectively no warning.
Health scoring was theatre. A quarterly spreadsheet exercise produced colour-coded statuses that nobody trusted, by the time it shipped, the underlying data was already 4–6 weeks stale.
CSMs lived in eight tabs. A typical customer review meant opening HubSpot, Zendesk, Stripe, Mixpanel, the usage dashboard, the renewal sheet, the QBR notes doc, and the Slack channel, for every account.
Forecasts were unreliable. The VP’s renewal forecast was built in a spreadsheet from CSM-submitted estimates; variance against actuals was running ±22% per quarter.
Playbooks didn’t run. The CS playbook library lived in Notion but was almost never triggered, because nothing automatically noticed when a playbook should fire.
Why Airtable
Loomstack evaluated three category-leading customer success platforms and Airtable. The decision came down to four factors: (1) Loomstack already had clean data sitting in HubSpot, Zendesk and Stripe, what they needed wasn’t another database, but a coordination layer on top, (2) the CS ops team wanted to own the system and iterate on it weekly without a vendor implementation queue, (3) the health-score formula needed to be theirs, not a vendor’s black box, and (4) integration with Slack was non-negotiable. Airtable’s linked-record model, sync sources, and native Slack actions made it the right fit.
Approach
How the implementation was run
We delivered the implementation in four phases over nine weeks. A core working group made up of the VP of Customer Success, two senior CSMs (one per segment), the head of CS Ops, and a RevOps analyst met twice a week to validate the data model, review prototypes, and approve each automation before it went live.
Phase
Weeks
What we did
Output
1. Discovery
1–2
Shadowed CSMs through 5 customer reviews. Audited HubSpot, Zendesk, Stripe and the usage dashboard. Workshopped the health-score formula with leadership.
Process map, data dictionary, agreed health-score definition.
2. Data model & sync
3–4
Designed 11-table relational schema. Configured one-way syncs from HubSpot (accounts/contacts), Stripe (subscriptions) and Zendesk (tickets).
Live Airtable base with synced data; ER diagram.
3. Interfaces & automations
5–7
Built the CSM Command Center, the Account Detail Workspace and the VP Forecast Room. Designed and tested 9 automations.
3 production interfaces; automation library.
4. Pilot, training, go-live
8–9
Piloted with 4 CSMs for one week, trained all 14 CSMs and the VP team, ran a 3-day shadow-mode period before full cutover.
Single-day cutover; 14 / 14 CSMs active on day 1.
Guiding principles
Don’t replace systems of record. HubSpot, Zendesk and Stripe stay as systems of record. Airtable is the system of action on top: pulling from each, surfacing what matters, and writing back where appropriate.
One health score, one definition. Health is a single formula owned by CS leadership, not three competing scores. Every place it appears in the workspace is the same calculation.
Surface signals, don’t bury them. An at-risk account should be impossible to miss. Risk surfaces on the CSM dashboard, the VP forecast, and as a Slack alert, within 24 hours of the signal.
CS Ops owns the base. The head of CS Ops and a RevOps analyst were trained as base administrators and now ship changes weekly without engineering or vendor involvement.
What Was Built
The Accounts table: the heart of the workspace
Every paying customer is a record in the Accounts table. From here, CSMs can see their book of business at a glance, sliced by segment, plan, health band, or renewal window. The grid below shows the production view used by the VP each Monday to triage the team’s at-risk accounts.
Image: Figure 1: The Accounts grid view, grouped by segment and coloured by health band. The footer rolls up total ARR and counts at-risk / critical accounts across the visible records.
Highlights of this view
Health is a coloured chip driven by a formula on the record. It’s the same calculation everywhere it’s shown.
ARR is a rollup from the linked Subscriptions table, which is one-way synced from Stripe, no double-entry, no drift.
CSM is a linked record to the Staff table, so rebalancing books happens in one place and propagates everywhere.
NPS (last) is a lookup from the most recent record in the NPS Responses table, refreshed automatically.
Two more views the team uses every day
Kanban by health band is the view the VP uses on Monday morning. Cards are stack-ordered by ARR, so the largest at-risk accounts always sit at the top. The colour bar on the left of each card encodes the band; the avatar in the corner is the CSM.
Image: Figure 2: Kanban view of the Accounts table, grouped by health band. Healthy / At risk / Critical / Onboarding / Churned columns make book composition obvious in one glance.
The expanded account record
Clicking any row opens the full account detail. This is the screen a CSM lives in during a customer review, every field a CSM might want is on one page, computed live from the underlying tables. The Health Score, Usage Trend, Open Tickets and Days to Renewal all turn red automatically when they cross their respective thresholds.
Image: Figure 3: The expanded record view for ENT-00142 (Halcyon Robotics). Notice that the timeline at the bottom shows the champion-change event detected on Apr 18 that automatically triggered a save playbook.
The Data Model
Eleven tables, one source of truth
The base is built around Accounts as the central entity. Everything else (contacts, subscriptions, tickets, health scores, renewals, playbooks, NPS responses) links back to an account. This single design decision is what makes the rollups, formulas and automations possible.
Image: Figure 4: The 11-table schema. Accounts is the hub; Subscriptions and Tickets are synced from Stripe and Zendesk respectively; the rest is native to Airtable.
Field-level decisions worth calling out
health_score is a single formula combining four sub-scores: usage (40%), support (25%), NPS (20%), commercial signals (15%). Weights live in a Settings table so leadership can tune them without code changes.
health_band is a derived formula (Healthy / At risk / Critical) with bands at 80 and 60, the only place the thresholds are defined in the entire workspace.
days_to_renewal is a formula reading from renewal_date; the renewal automations all read this field, so date arithmetic lives in one place.
Subscriptions is one-way synced from Stripe: Airtable doesn’t write back. ARR is rolled up from synced rows.
Tickets is one-way synced from Zendesk every 15 minutes, open_tickets on the Account is a count rollup.
Interfaces
Three role-based interfaces, one underlying base
Different people on the CS org need different things from the same data. We built three Interfaces on top of the base, each tailored to a role. All three read from and write to the same tables. There is no duplication.
The CSM Command Center
This is the screen a CSM opens when they sit down at their desk. It surfaces the four KPIs that matter to them, the live health distribution of their book, a renewal forecast for the next six months, and a prioritised list of accounts that need attention today, all filtered to their book automatically.
Image: Figure 5: The CSM Command Center, scoped to the logged-in CSM. KPI cards on top; health distribution and renewal forecast charts below; on the right, the accounts that need attention today, sorted by ARR × risk.
Two other interfaces
Account Detail Workspace: the single screen a CSM uses during a customer review. Live health timeline, ticket history, usage chart, contacts directory, active playbooks, and a notes stream, all on one scrollable page.
VP Forecast Room: read-only org-wide view for CS leadership. Net revenue retention trend, weighted renewal forecast by quarter, churn-risk dollar amount, and CSM utilisation. Replaces what used to be a manual Friday afternoon spreadsheet exercise.
Automations
Automating the work that catches churn early
Nine automations remove roughly 3,700 manual touchpoints from the team’s month, but more importantly, they catch churn signals while there’s still time to act. None of them replace a CSM’s judgment; they make sure no signal slips through.
Image: Figure 6: The Automations panel. Nine automations, eight currently enabled, with run counts visible per automation. The detail pane on the right shows the trigger and three actions of the renewal-playbook workflow.
The automation catalogue
Each automation is owned by a named person on the CS Ops team, documented in a Notion runbook, and reviewed monthly.
#
Automation
Trigger
Actions
Replaces
1
Health score recompute (nightly)
Schedule: 2:00 AM, every night
Runs the health-score formula across all accounts; writes a row into Health Scores; updates health_band.
Quarterly spreadsheet exercise
2
Score drop > 15 pts → Alert CSM
Health Scores.delta_30d < −15
Slack DM to CSM + tags account as ‘momentum loss’; logs into the activity timeline.
Nothing: drops were missed
3
Renewal in 90/60/30 days → Playbook
Accounts.days_to_renewal IN (90, 60, 30)
Branches on plan + health to pick playbook (e.g. Enterprise + <80 → exec save). Spawns 5 linked tasks.
Manual playbook lookup in Notion
4
Trial ending in 7 days → Sales handoff
Subscriptions.status = trial AND term_end in 7 days
Creates handoff record; assigns AE; Slack DM to AE with usage summary; books a calendar slot.
CSM-to-AE handoff via email
5
New ticket P1 → Slack war room
Tickets.priority = P1 (synced from Zendesk)
Posts to #cs-warroom; tags CSM, AE, support engineer; creates a Linear issue if flagged.
Manual Slack post by support
6
NPS detractor → Recovery sequence
NPS Responses.score ≤ 6
Sets account flag; emails CSM with NPS comment; creates a follow-up task due in 48 hours.
NPS responses going unread
7
Usage anomaly → Tag account
Usage signal: WAU drop > 25% w/w
Sets a ‘usage_drop’ tag; surfaces it on CSM dashboard ‘Needs attention’ panel.
Caught (sometimes) at quarter-end
8
Onboarding day 14/30/60 check-ins
Schedule: nightly scan of accounts in Onboarding
Spawns the right check-in task at each milestone; emails customer with the day-30 product-tour link.
Three different CSM templates
9
Champion change detected → Playbook
Contacts.is_primary changes OR HubSpot role-change signal
Creates ‘Champion change’ playbook record; spawns a 5-step exec-meeting save plan.
Nothing: usually noticed too late
Worked example: automation #3 in detail
Trigger: nightly check on every Account where days_to_renewal equals exactly 90, 60, or 30 and status = Active.
Action 1 (Conditional branch): evaluates plan + health. Enterprise + health < 80 routes to Playbook A: Executive Save; everything else routes to Playbook B: Standard Renewal.
Action 2 (Create Playbook + tasks): writes a row in the Playbooks table linked to the account, then spawns 5 tasks with due dates working back from the renewal date.
Action 3 (Notify): Slack DM to the CSM with a deep link to the account; appends the record to the VP’s weekly forecast view.
Replaced: CSMs manually checking renewal calendars and looking up playbooks in Notion (frequently the wrong playbook, frequently too late).
Integrations
How the workspace connects to the SaaS stack
Airtable doesn’t replace HubSpot, Zendesk, Stripe, or the product analytics warehouse. It coordinates them.
System
Direction
What flows
Mechanism
HubSpot
One-way (in)
Accounts and Contacts sync nightly. Field-level mapping for industry, employee count, lifecycle stage and owner.
Airtable HubSpot sync source
Stripe
One-way (in)
Subscription rows sync every 4 hours. ARR rolls up from subscription records to the account.
Stripe → Airtable via Zapier
Zendesk
One-way (in)
Tickets sync every 15 minutes. Priority, status, assignee, and time-open are all rollup-able.
Native Zendesk sync
Slack
One-way (out)
Health drops, P1 tickets, renewal-playbook triggers, NPS detractor alerts.
Native Slack action in Automations
Mixpanel / Snowflake
One-way (in)
Daily WAU and feature-adoption metrics per account, used as inputs to the health score.
Snowflake → Airtable via reverse-ETL
Linear
One-way (out)
P1 tickets that need engineering escalation create a Linear issue with customer context attached.
Linear API call from script action
Gmail / Outlook
One-way (out)
Customer-facing emails (NPS follow-ups, day-30 onboarding tour) sent from automation actions.
Native email actions
Permissions & data handling
Workspace plan: Airtable Business with SAML SSO via the company’s identity provider. Two-factor authentication enforced for all users.
Field-level permissions: ARR and renewal amounts are read-only for CSMs and hidden from support engineers; visible to AEs and the VP.
Book-level filtering: CSMs see only their own accounts by default on every interface; the VP sees the full org. No CSM can edit another CSM’s accounts.
Audit: Airtable’s revision history is retained, and a nightly backup is exported to the company’s S3 bucket via the Airtable API.
Results
Measurable change after 90 days in production
Metric
Before
After
Change
Net revenue retention (NRR)
102%
114%
+12 pts
Average risk-detection lag
51 days
4 days
−92%
Renewal forecast variance
± 22%
± 6%
−73%
At-risk ARR identified earlier
n/a
$1.2M (Q1)
+ saved revenue
CSM time spent on reporting
≈ 6 hr / week
≈ 45 min / week
−87%
Playbook trigger rate
≈ 14% of cases
100% of cases
+ coverage
Automated touchpoints / month
0
3,742 across 8 active flows
n/a
CSM adoption (firm-wide)
n/a
14 / 14 active on day 1
100%
In the words of the team
The thing that changed isn’t really the dashboard: it’s that the team has time. My CSMs aren’t building reports any more, they’re actually talking to customers. And when an account starts going sideways, we know about it the next morning, not three weeks later when the renewal blows up. We saved $1.2M of ARR last quarter that would otherwise have walked out the door.
What we’d do again, and what we’d change
Worked well: co-designing the health-score formula with leadership in week 1. Because they owned it from day one, nobody argued with the scores once they started shipping.
Worked well: shipping the CSM Command Center first and the VP Forecast Room last. Adoption follows the front line.
Would change: we underestimated the data-cleanup burden in HubSpot during phase 2. About 11% of accounts had missing or stale ownership; cleaning that up consumed three days we hadn’t planned for.
Would change: add an in-product feedback widget that writes directly to the NPS Responses table. The current Typeform-based flow has a higher drop-off than we’d like.
AI-powered SaaS platform for Dubai real estate brokerages. Capture inbound leads, classify with GPT-4o, auto-reply via WhatsApp, assign to agents, and track follow-ups, all behind Stripe subscription billing.
Section: Custom SaaS Platforms
Image: The LeadFlow AI landing page: “Never miss a hot lead again”.
Type: Product Case Study, SaaS Platform
Product: LeadFlow AI, Dubai Real Estate Lead Automation
Industry: Proptech / Real Estate (Dubai, UAE)
Frontend: React (Vite) + Tailwind CSS, hosted on Vercel
Backend: Node.js + Express, hosted on Render
Database: Firebase Firestore + Firebase Auth
AI Engine: OpenAI GPT-4o
Messaging: WhatsApp Cloud API (Meta Graph API v21.0)
Payments: Stripe (AED subscriptions with 7-day trial)
Why we built this
Dubai’s real estate market moves fast. Leads come in around the clock from buyers, renters, and investors often in multiple languages, often with a specific unit, area, or budget in mind. Brokerages that respond within minutes close deals; those that respond the next day don’t.
The core problems Dubai brokerages were facing:
No consistent lead qualification: every agent applied their own judgment to decide which leads to call first, leading to hot buyers being left to wait while cold inquiries were chased
Response time was too slow: manually reading, sorting, and replying to leads during business hours meant international prospects in different time zones received no reply at all
WhatsApp was unmanaged: the primary communication channel in the UAE was handled informally, with no message history, no threading, and no record-keeping in the CRM
Language was a barrier: Dubai’s buyer pool is global; leads arrived in Arabic, Russian, Chinese, French, and English, and most brokerages had no way to reply professionally in anything other than English
Agent workload was unbalanced: lead distribution depended on who was online or who a manager happened to message, with no fair rotation system
Follow-up fell through the cracks: with no automated reminders, leads that needed a call in three days were simply forgotten
The goal was to build a turnkey SaaS platform that any Dubai brokerage could subscribe to, embed a lead capture form on their website, and immediately have every inbound enquiry processed, classified, replied to, and assigned, automatically, in any language, within seconds.
What this system does
LeadFlow AI is a multi-tenant SaaS platform that gives Dubai real estate brokerages two things: an automated lead pipeline and a dashboard for managing it. Every brokerage gets its own isolated data environment, its own agent team, and its own Stripe subscription.
Automated lead pipeline
Accepts inbound leads via a REST API endpoint that can be embedded into any website contact form, landing page, or chatbot
Validates and checks the brokerage subscription before processing: if the Basic plan’s 100-lead monthly limit is reached, the endpoint returns a clear upgrade prompt
Sends the lead through GPT-4o for classification: language detection, intent classification (buy / rent / invest / general inquiry), temperature scoring (hot/warm / cold), and AI-generated reply
Assigns the lead to an agent via atomic round-robin rotation, a Firestore transaction ensures fair distribution even under concurrent submissions
Sends the AI-generated reply directly to the lead’s WhatsApp number via the Meta Cloud API, in the same language the lead wrote in
Stores the complete lead record, AI analysis, and message thread in Firestore for full history
AI engine (GPT-4o)
System prompt defines a “premium Dubai luxury real estate” tone, professional, no emojis, no slang, concise but warm
Detects lead language automatically (ISO 639-1 codes: en, ar, ru, zh, fr, and others) and replies in that language
Classifies intent as buy, rent, invest, or inquiry based on message content
Tags lead temperature: hot (specific budget/area / timeline), warm (genuine interest, lacks specifics), or cold (general browsing)
Runs at temperature 0.4 with JSON response format enforced, consistent, structured output on every call
Falls back to a safe default reply if the AI call fails, ensuring no lead is ever left without an acknowledgement
WhatsApp integration (Meta Cloud API)
Outbound: sends AI replies and agent messages to leads via the WhatsApp Cloud API (Meta Graph API v21.0)
Inbound: receives replies from leads via a verified Meta webhook, matches the sender’s phone number to the correct lead record, and stores the message in the thread
Webhook verification uses Meta’s challenge-response flow with a custom verify token
All incoming payloads are validated via HMAC-SHA256 signature using the Meta App Secret before processing
Phone numbers are cleaned (stripped of spaces, dashes, and plus signs) before every API call
Owner dashboard
Displays total leads, hot leads count, follow-ups due today, and new leads this week
Shows agent performance table: total leads assigned vs. leads closed per agent
Lists the 10 most recent leads with full status, temperature, and intent data
The owner can reassign leads between agents directly from the dashboard
Visible only to users with the owner role: enforced server-side via requireRole middleware
Agent dashboard
Shows assigned leads, active (non-closed) leads, and follow-ups currently due
Lists the agent’s 10 most recent assigned leads
Agents can only view and interact with leads assigned to them, enforced in every route handler
Agents can send messages to leads, update lead status, and set follow-up dates
Subscription billing (Stripe)
Two plans available: Basic (AED 149/month, 100 leads) and Pro (AED 399/month, unlimited leads + analytics)
Both plans include a 7-day free trial before billing begins
Stripe Checkout handles the payment flow; Stripe Billing Portal handles plan changes and cancellations
Stripe webhooks update Firestore user records in real time on subscription activation, update, and cancellation
Agents automatically inherit their owner’s subscription status: if the owner’s plan lapses, agents lose access too
Follow-up automation
Agents set a follow-up date on any lead directly from the lead detail page
A scheduled cron job runs every day at 8:00 AM UAE time (UTC+4) via node-cron
Any lead with a follow-up date on or before today and a non-closed status is flagged followUpDue: true in Firestore
The agent dashboard surfaces these flags as a follow-up due count, creating a daily task list without manual tracking
Benefits
Instant first response at any hour: every lead gets a professional WhatsApp reply within seconds, regardless of time zone or day of the week
Replies in the lead’s language: Arabic, Russian, Chinese, French, and English buyers all receive a response in their own language without any manual effort
Fair, automatic agent assignment: round-robin distribution via Firestore transactions means no agent is overlooked and no manager needs to manually route leads
Hot leads surface immediately: buyers who mention a specific budget, area, or timeline are flagged HOT so agents know exactly who to call first
Complete conversation history: every WhatsApp message in and out is stored against the lead record, giving agents full context before they pick up the phone
No missed follow-ups: the daily cron job ensures agents see overdue leads on their dashboard each morning without any manual diary management
Subscription-gated access: brokerages pay per month; if a plan lapses, access is revoked automatically via Stripe webhook sync
Multi-tenant by design: each brokerage’s data is isolated by ownerUid; no lead or agent data is ever visible across brokerage accounts
How it works (simplified)
A lead submits a form on the brokerage’s website, the form POSTs to the public /api/leads endpoint with name, email, phone, message, and ownerUid
Subscription and plan limits are checked: the server verifies the brokerage has an active or trialing Stripe subscription, and that the Basic plan lead cap has not been reached
GPT-4o processes the lead: language is detected, intent classified, temperature scored, and a professional reply generated in the lead’s own language
The lead is assigned to an agent: a Firestore transaction reads the round-robin counter, selects the next agent in the rotation, and atomically increments the counter
The lead record is written to Firestore, all fields stored: name, contact details, message, AI analysis (language, intent, temperature), assigned agent, status, and timestamps
The AI reply is sent via WhatsApp: if a phone number was provided, the generated reply is dispatched through the Meta Cloud API, and the WhatsApp message ID is stored
The API returns a success response: the caller receives the lead ID, AI reply text, intent, temperature, and assigned agent ID
The agent is notified via their dashboard: the new lead appears in their assigned leads list with full AI analysis visible
If the lead replies on WhatsApp: the message arrives at the Meta webhook, is validated, matched to the lead by phone number, and stored in the messages collection
Follow-up dates are tracked automatically: the daily cron job at 8 AM UAE time flags any lead whose follow-up date has arrived, surfacing it on the agent’s dashboard
System architecture (simplified)
Image: Architecture: inbound leads reach a public API and pass a five-step pipeline (plan limits, GPT-4o classification and reply, round-robin agent assignment, Firestore write, WhatsApp reply), with a Stripe billing layer and a daily follow-up job.
Subscription plans
LeadFlow AI is sold as a monthly SaaS subscription in UAE Dirhams. Both plans include a 7-day free trial before the first charge. Stripe Checkout handles the payment flow; Stripe Billing Portal handles self-service plan changes and cancellations.
Plan
Price
Lead Limit
Included Features
Basic
AED 149 / month
100 leads / month
AI classification, WhatsApp auto-reply, agent assignment, follow-up tracking
Pro
AED 399 / month
Unlimited leads
Everything in Basic + analytics dashboard, agent performance tracking, priority support
The Basic plan lead limit is enforced server-side on every lead submission. When the 100-lead monthly cap is reached, the API returns an HTTP 429 response with a clear upgrade message. Pro plan subscribers have no cap.
Dashboard overview
Owner dashboard
The owner's view is the brokerage’s operational command centre. It shows four headline KPIs (total leads, hot leads, follow-ups today, new leads this week), an agent performance breakdown, and a live feed of the most recent leads. Owners can reassign any lead to a different agent directly from this view.
Role-restricted: visible only to owner accounts
Agent dashboard
The agent's view is designed for daily use. It shows the agent’s assigned lead count, active (non-closed) lead count, and follow-ups currently due, the three numbers that define their working day. Below the KPIs is a list of their most recently assigned leads.
Agents only see leads assigned to them: enforced at the API level
Lead detail page
Each lead has a dedicated detail page showing all contact information, AI classification results (language, intent, temperature), the full WhatsApp message thread, and status controls. Agents can update the lead’s status, set or clear a follow-up date, and send new messages directly from this page.
Message thread includes AI replies, agent messages, and incoming WhatsApp replies in chronological order
Pricing page
Public-facing pricing page displays both plans with feature lists and a “Start Free Trial” call-to-action. Clicking a plan triggers Stripe Checkout in AED, with the 7-day trial clearly communicated before any payment details are entered.
Currency displayed in AED; trial communicated pre-checkout
Lead classification & AI scoring
Every inbound lead is processed by GPT-4o using a structured system prompt that enforces a consistent professional tone and returns strict JSON. The model evaluates four dimensions simultaneously and returns them in a single API call.
Temperature
Score
Routing Action
Classification Criteria
HOT
High
Immediate agent assignment + WhatsApp reply
Mentions specific budget, area, or timeline. Clearly ready to act.
WARM
Medium
Assigned + follow-up date set
Shows genuine interest but lacks specifics about budget, area, or timing.
COLD
Low
Stored, lower priority nurture
General inquiry, browsing behaviour, or unclear intent.
The AI prompt explicitly defines the classification rules for each temperature band, prohibits emojis and slang, and instructs the model to respond in the detected language of the lead’s message. The system prompt is fixed (not user-editable) to ensure brand consistency across all brokerages.
Project evolution & key milestones
The platform scope expanded significantly from an initial lead capture proof-of-concept. Key additions and refinements delivered across the build:
AI & classification
GPT-4 integration upgraded to GPT-4o for higher accuracy and lower latency
Temperature set to 0.4 after testing: lower values produced overly rigid replies; higher values produced inconsistent classification
Fallback reply added, so no lead is ever unanswered if the OpenAI API is unavailable
Intent classification expanded from buy/rent to include invest and inquiry as distinct categories
WhatsApp integration
Initial outbound-only integration expanded to support two-way messaging via the Meta webhook
Incoming message matching logic added: sender phone matched to the most recent lead record in Firestore
HMAC-SHA256 signature validation added using the Meta App Secret for all incoming webhook payloads
Phone number cleaning function added to handle +, spaces, and dashes from various form inputs
Agent assignment
Initial random assignment replaced with deterministic round-robin using a Firestore atomic counter transaction
Counter document stored per brokerage (assignment_{ownerUid}) to ensure correct rotation per team
Graceful fallback: if no agents exist, the lead is stored unassigned rather than failing
Subscription billing
Stripe integration added with two recurring AED products (Basic and Pro)
7-day free trial added to subscription_data at checkout session creation
Stripe webhook handler added for checkout.session.completed, customer.subscription.updated, and customer.subscription.deleted events
Subscription status synced to Firestore user record in real time on every Stripe event
Basic plan 100-lead monthly cap enforced server-side via Firestore count() query
Stripe Billing Portal endpoint added for self-service plan management
Security & access control
Firebase Auth middleware added to all protected routes; public lead capture endpoint intentionally left unprotected
requireSubscription middleware added to check subscription status for both owners and agents (agents inherit from the owner)
requireRole middleware added for owner-only routes (owner dashboard, agent management)
Raw body preservation implemented for Stripe and WhatsApp webhook routes (bypassing Express JSON parsing)
Follow-up system
followUpDate field added to lead schema; agents can set it from the lead detail page
Daily cron job added via node-cron: runs at 8:00 AM UAE time (UTC+4 = 04:00 UTC)
Batch Firestore update sets followUpDue: true on all leads whose date has arrived, for efficient dashboard display
Data flow architecture
A reference overview of how data moves through each stage of the system and at what cadence:
Stage
Source
Destination
Trigger / Cadence
Lead intake
Website form / API call
Express backend
On submission (real-time)
AI processing
OpenAI GPT-4o
Firestore lead record
Per lead, synchronous
Agent assignment
Firestore counters (round-robin)
Lead record (assignedAgentId)
Per lead, synchronous
WhatsApp reply
AI-generated reply text
Lead's phone (Meta API)
Per lead, async
Follow-up check
Firestore leads collection
followUpDue flag on each lead
Daily at 8:00 AM UAE time
Dashboard refresh
Firestore (Realtime queries)
Owner / Agent dashboard
On page load / request
Subscription sync
Stripe webhook events
Firestore user record
On Stripe event (real-time)
Note
Firestore is the single source of truth for all lead records, message threads, user profiles, subscription status, and round-robin counters. The Firebase Admin SDK (backend) and Firebase Web SDK (frontend) both read from the same Firestore project, with security rules and server-side middleware providing access control.
API reference
All endpoints are prefixed with /api/. Protected routes require a Firebase ID token in the Authorization: Bearer <token> header. The lead capture endpoint (/api/leads POST) is intentionally public to allow embedding on any brokerage website without authentication.
Method
Endpoint
Description
POST
/api/leads
Public lead capture: no auth required. Runs AI, assigns agent, sends WhatsApp.
GET
/api/leads
List leads (owners see all, agents see assigned only). Filterable by status, temperature, intent.
GET
/api/leads/:id
Single lead detail with access control (role-based).
PATCH
/api/leads/:id
Update lead status, follow-up date, or agent assignment.
GET
/api/leads/:id/messages
Retrieve full message thread for a lead.
POST
/api/leads/:id/messages
Agent sends a message, delivered via WhatsApp if phone is available.
GET
/api/dashboard/owner
Owner dashboard: total leads, hot leads, follow-ups today, agent performance.
GET
/api/dashboard/agent
Agent dashboard: assigned leads, active leads, follow-ups due.
POST
/api/stripe/create-checkout
Create Stripe Checkout session (Basic or Pro plan, AED, 7-day trial).
POST
/api/stripe/portal
Create Stripe Billing Portal session for plan management.
Meta webhook verification challenge (subscribe mode).
POST
/api/whatsapp/webhook
Receive incoming WhatsApp messages, match to lead by phone, store in messages collection.
GET
/api/health
Health check: returns server status and timestamp.
Security
LeadFlow AI is built with multi-tenant security as a first-class requirement. Each security layer is implemented independently so that no single point of failure exposes brokerage data.
Security Layer
Implementation
Firebase ID Token Auth
All protected routes verify Bearer token via Firebase Admin SDK. Invalid or expired tokens return 401.
Subscription Gate
Middleware checks owner subscription status (active / trialing) before allowing access. Agents inherit their owner’s subscription.
Role-Based Access Control
requireRole(‘owner’ | ‘agent’) middleware enforces role boundaries. Agents can only access their assigned leads.
WhatsApp Signature Validation
Incoming Meta webhooks validated via HMAC-SHA256 using App Secret. Invalid signatures return 401.
Stripe Webhook Verification
Stripe events verified using stripe.webhooks.constructEvent with the webhook signing secret.
Helmet HTTP Headers
Sets security headers (XSS, clickjacking, content-type sniffing) via the Helmet middleware.
CORS Restriction
Requests accepted only from the configured FRONTEND_URL. Credentials are required for authenticated calls.
Plan Limit Enforcement
Basic plan lead cap (100/month) enforced server-side via Firestore count query on every lead submission.
Image: The pricing page: a Basic plan at AED 149 per month and a Pro plan at AED 399 per month, both with a 7-day free trial.
Image: The live demo: a sample enquiry is classified by GPT-4o as a HOT lead, with intent, urgency, language and an AI-generated WhatsApp reply.
A full-stack MERN real estate operations platform with CRUD, Google Auth, live charts, and Cloudinary image management, built on the Refine framework with Material UI.
Section: Custom SaaS Platforms
Image: The Estato dashboard: revenue, property, agent and customer tiles, a monthly revenue chart and a property referrals breakdown.
Real estate teams at any scale face the same structural problem: their properties, agents, and performance data live in disconnected places. Listing portals hold property records. Spreadsheets track agent performance. Photos are stored in email threads or local drives. There is no single screen that shows the full picture.
The core problems Estato was designed to solve:
No centralised property database: listings were scattered across portals and spreadsheets with no consistent schema for type, location, price, or description
Agent visibility was absent: managers had no way to see, at a glance, which agents held the most listings or how the portfolio was distributed across the team
Creating and editing listings was slow: no standard form, no structured property types, and no direct photo upload meant listings were inconsistent and time-consuming to publish
No dashboard-level KPIs: total properties for sale, total for rent, total customers, and total agents were metrics that required manual counting across multiple systems
No referral source insight: the team could not measure which channels (social media, marketplaces, websites, digital ads) were driving enquiries
Authentication was unmanaged: agents accessed tools via shared passwords with no individual accountability and no way to tie listings back to a specific user
The goal was a unified MERN-stack dashboard where every agent logs in with Google, can create and manage their own listings with photo upload, and managers can see live KPIs, revenue trends, and the full agent roster in one place.
What this system does
Estato is a full-stack MERN application that gives real estate teams a complete operations interface: live analytics, a full property CRUD system, an agent directory, individual profiles, and Google-authenticated access. The Refine framework handles data fetching, routing, and auth integration; Material UI provides the component layer; ApexCharts and custom donut charts deliver the analytics.
Operations dashboard
Four donut-style KPI tiles at the top of the home page give instant visibility: Properties for Sale (live count from the API), Properties for Rent, Total Customers, and Total Agents, each with a percentage ring showing the proportion against a target
Total Revenue area chart (ApexCharts) renders monthly revenue data across the full year with a smooth curve and gradient fill
Property Referrals horizontal bar chart shows the contribution of each lead source: Social Media (64%), Digital Ads (80%), Websites (50%), Marketplace (40%), and Others (15%).
Latest Properties section beneath the charts shows the 4 most recently added properties as live cards pulled from the API in real time
Property management (full CRUD)
All Properties page renders the full listing catalogue as a responsive card grid, with live search by title, filter by 8 property types (Apartment, Villa, Farmhouse, Condos, Townhouse, Chalet, Studio, Duplex), price sort toggle, and server-side pagination with a configurable page-size selector (5, 10, 20, 30, 40, 50, All)
Property Detail page shows the full-size property photo, property type with a 5-star rating, title, location, price, and description on the left. The right sidebar shows the creator agent’s avatar, name, property count, and location. Context-aware action buttons adapt based on the logged-in user: Message and Call for other agents’ listings; Edit and Delete for the current user’s own listings
Create Property form collects title, property type, description, location, price, and photo. The photo is read into base64 via the FileReader API and submitted to the backend, which uploads it to Cloudinary and stores the CDN URL
Edit Property form pre-populates all existing field values and allows any field to be changed, including the photo. If the photo is unchanged, the existing Cloudinary URL is preserved
Delete uses a Mongoose session transaction to remove the property document and simultaneously pull it from the creator’s allProperties array, keeping both collections in sync atomically
Agent management
Agents List page displays every registered user as an agent card with their Google avatar, name, email address, and total property count
Agent Profile page shows the full agent detail view via the shared Profile component, including the agent’s complete property portfolio
My Profile page uses the same shared Profile component for the currently authenticated user, with data fetched by their own MongoDB user ID
Authentication
The login page renders a Google Sign-In button using the Google Identity Services API. The credential response is a signed JWT
parseJwt utility decodes the Google JWT on the client to extract name, email, avatar (picture), and sub (user ID) from the payload
On every login, the user profile is POST-ed to /api/v1/users, which is idempotent: it returns the existing MongoDB document if the email already exists, or creates a new user record if not
The raw Google credential token is stored in localStorage as token. Axios’s request interceptor attaches it as Authorization: Bearer <token> to every subsequent API call
On logout, token and user are removed from localStorage and google.accounts.id.revoke() is called to invalidate the Google session
Light and dark mode
ColorModeContextProvider wraps the entire application and exposes a setColorMode function through React context
The current mode is stored in localStorage to persist the user’s preference across sessions. On first load, the system’s prefers-color-scheme media query is checked to set the default
Two custom Material UI themes are defined: overridedLightTheme and overridedDarkTheme, each extending Refine’s built-in theme objects with custom palette overrides. The active theme is passed to ThemeProvider at runtime
Benefits
Single source of truth for the portfolio: every property, agent, and relationship lives in one MongoDB database with a consistent schema, accessible from any device
Live KPIs on every login: four dashboard tiles give the team instant situational awareness without opening a single spreadsheet
Consistent listing creation: the structured form with defined property types ensures every listing is created to the same standard, with a real photo uploaded directly to Cloudinary
Role-aware property actions: agents can only edit or delete their own listings; other agents’ listings show contact options instead
Atomic data integrity: create and delete operations use Mongoose session transactions to keep the Property collection and the User.allProperties array perfectly synchronised
Zero password management: Google OAuth means no passwords to store, reset, or manage; every agent has their own authenticated identity
Scalable Refine architecture: adding a new resource (reviews, messages, enquiries) requires registering it in App.tsx; the data provider, routing, and UI scaffolding are automatically available
Dark/light mode out of the box: system preference is respected on first load; the preference is then stored in localStorage and toggleable at any time
How it works (simplified)
Agent visits the login page and clicks ‘Sign in with Google’. Google Identity Services returns a signed JWT credential
parseJwt decodes the JWT client-side to extract the user’s name, email, and avatar. The profile is POST-ed to /api/v1/users, which creates or finds the user in MongoDB
Token stored in localStorage; Axios interceptor attaches it as a Bearer token to every subsequent request. Refine’s checkAuth hook reads localStorage on each route change to decide whether to redirect to login
Dashboard loads and fires a useList call to /api/v1/properties (pageSize: 4) for the Latest Properties grid. The 4 KPI donut tiles and both ApexCharts components render with static or aggregate data
All Properties page uses Refine’s useTable hook, which manages current page, page size, sorter, and filters as state and translates them into query params on the API call (_start, _end, _sort, _order, title_like, propertyType)
Creating a property: the agent fills the form. On photo selection, FileReader reads the file as a base64 data URL. On submit, onFinish POSTs the full payload (including the base64 photo and the agent’s email) to /api/v1/properties
Server receives the POST, finds the user by email, uploads the base64 photo to Cloudinary, receives back a CDN URL, then opens a Mongoose session and atomically creates the Property document and pushes its _id to user.allProperties
Property Detail page calls useShow to fetch /api/v1/properties/:id with the creator user document populated. Action buttons (Edit/Delete vs Message/Call) are conditionally rendered by comparing user.email with propertyDetails.creator.email
Deleting a property opens a browser confirmation dialog. On confirm, useDelete fires a DELETE request. The server opens a Mongoose session, removes the Property document, and pulls its _id from creator.allProperties, then commits the transaction
Agent and profile pages call useList / useShow against /api/v1/users, which returns user documents with all properties populated. The shared Profile component renders the same layout for both Agent Profile and My Profile
System architecture
Image: Architecture: a React 18 and TypeScript browser app built on Refine with Material UI talks over HTTPS to an Express server that uses MongoDB Atlas and Cloudinary.
Page inventory
Estato consists of 9 pages, each a React TypeScript component managed by Refine’s routing and data layer.
Page
File
Key Content
Dashboard (Home)
home.tsx
4 PieChart KPI tiles, TotalRevenue ApexChart, PropertyReferrals bar chart, Latest Properties card grid (4 items live from API).
All Properties
all-properties.tsx
Full property grid with search by title, filter by 8 property types, sort by price (asc/desc), page size selector, Previous/Next pagination.
Property Details
property-details.tsx
Full-width photo, property type + 5-star rating, title, location, price, description. Agent sidebar: avatar, name, property count, location, Google Map, Book Now, Message/Edit + Call/Delete buttons.
Create Property
create-property.tsx
Delegates to shared Form component. Handles image file selection (FileReader to base64), attaches logged-in user email before submitting to API.
Edit Property
edit-property.tsx
Same shared Form component as Create. Pre-populates existing property data. Allows photo re-upload; photo URL preserved if unchanged.
Agents List
agent.tsx
Grid of all users (agents) from /api/v1/users. Each AgentCard shows avatar, name, email, and property count. Clicking navigates to agent profile.
Agent Profile
agent-profile.tsx
Full agent detail using shared Profile component. Shows avatar, name, email, properties count and full property list.
My Profile
my-profile.tsx
Logged-in user’s own profile using the same shared Profile component. Data fetched from /api/v1/users/:userId.
Login
login.tsx
Google Sign-In button using Google Identity Services. On success, JWT decoded, user POSTed to /api/v1/users, token stored in localStorage.
Data models
Two Mongoose schemas define the complete data layer. The bidirectional relationship between Property and User is maintained by the server using session transactions on create and delete operations.
Collection
Field
Type
Notes
Property
title
String (required)
Listing headline
Property
description
String (required)
Full description text
Property
propertyType
String (required)
Apartment, Villa, Farmhouse, Condos, Townhouse, Chalet, Studio, or Duplex
Property
location
String (required)
Free-text location / address
Property
price
Number (required)
Numeric value (rendered as $price per day in the UI)
Property
photo
String (required)
Cloudinary CDN URL after base64 upload
Property
creator
ObjectId → User
Populated on read to include full user document
User
name
String (required)
Full name from Google profile
User
email
String (required)
Google account email; used as unique identifier
User
avatar
String (required)
Google profile picture URL
User
allProperties
[ObjectId] → Property
Bidirectional reference array; maintained via Mongoose transactions
Note
The User schema does not require a password field because authentication is handled entirely by Google OAuth. The email address serves as the unique identifier and is used both for login matching and for associating new properties with the correct creator.
API reference
All endpoints are prefixed with /api/v1/ and served by the Express server on port 8080. No authentication middleware is currently applied server-side; auth is enforced client-side by Refine’s checkAuth hook and the Axios Bearer token interceptor.
Method
Endpoint
Description
GET
/api/v1/properties
List all properties. Supports _start, _end, _sort, _order, title_like, propertyType query params. Returns x-total-count header for Refine pagination.
GET
/api/v1/properties/:id
Fetch a single property with creator user populated (name, email, avatar, allProperties).
POST
/api/v1/properties
Create a new property. Uploads photo to Cloudinary via base64. Uses Mongoose session transaction to atomically create property and push to user.allProperties.
PATCH
/api/v1/properties/:id
Update an existing property. Re-uploads photo to Cloudinary. Falls back to original photo URL if upload fails.
DELETE
/api/v1/properties/:id
Delete a property. Uses Mongoose session transaction to remove from DB and pull from creator’s allProperties array atomically.
GET
/api/v1/users
List all users (agents). Supports _end for limit. Returns full user documents including allProperties references.
POST
/api/v1/users
Create or find a user by email (idempotent). Called on every Google OAuth login to ensure the user record exists in MongoDB.
GET
/api/v1/users/:id
Fetch a single user by MongoDB _id with allProperties populated. Used by agent profile and my profile pages.
Dashboard overview
Home: KPI dashboard
The home page opens with four donut-style KPI tiles: Properties for Sale (live count from the API), Properties for Rent, Total Customers (5,684), and Total Agents (684). Each tile uses a two-segment ApexCharts donut series to visualise the proportion as a ring. Below the tiles, a TotalRevenue area chart spans the full width alongside a PropertyReferrals horizontal bar chart.
Route: / (DashboardPage)
All Properties
The properties page renders the full listing catalogue as a card grid. The toolbar above the grid provides a Sort price button (toggles asc/desc), a title search text field, and a property type dropdown filter. Pagination controls at the bottom include Previous/Next buttons, a current page indicator, and a page-size selector. All filter, sort, and page state is managed by Refine’s useTable hook and translated into API query parameters automatically.
Route: /properties
Property Details
The detail page is a two-column layout. The left column shows the full-width property photo (546px height), property type, a 5-star rating row, title, location pin with text, price in blue, and a description block. The right sidebar contains the agent card (Google avatar, name, agent role, location, property count) followed by action buttons that are context-aware: Message + Call for listings the current user does not own; Edit + Delete for their own listings. A Google Maps screenshot and a Book Now button fill the remaining sidebar space.
Route: /properties/show/:id
Create / Edit Property
The shared Form component renders the same layout for both Create and Edit. Fields: Title, Property Type (dropdown: Apartment, Villa, Farmhouse, Condos, Townhouse, Chalet, Studio, Duplex), Description, Location, Price, and Photo (file input). On Edit, all fields are pre-populated from the existing record. The photo preview renders below the file input once a file is selected. Form submission is handled by Refine’s useForm hook via @pankod/refine-react-hook-form.
Route: /properties/create | /properties/edit/:id
Agents List
The agents page uses useList against /api/v1/users to fetch all registered users. Each AgentCard displays the agent’s Google avatar, full name, email address, and total number of properties they have listed. Clicking a card navigates to that agent’s full profile.
Route: /agents
Agent Profile / My Profile
Both the agent profile and my profile use the same shared Profile component. It fetches the user by ID from /api/v1/users/:id with allProperties populated, and renders the agent’s full detail view, including their complete property portfolio. My Profile loads the currently authenticated user’s own data using their userId from localStorage.
Route: /agents/show/:id | /my-profile
Technology stack
The complete dependency surface for both client and server packages:
Layer
Package / Version
Role
UI Framework
@pankod/refine-core ^3.103
Data fetching (useList, useShow, useDelete, useGetIdentity), auth provider, routing
Component Library
@pankod/refine-mui ^3.63
Material UI integration for Refine: layout, notifications, theme provider
Forms
@pankod/refine-react-hook-form
react-hook-form integration for create/edit property forms with Refine’s onFinish lifecycle
Routing
@pankod/refine-react-router-v6
React Router v6 adapter; all navigation uses useNavigate, useParams
Data Provider
@pankod/refine-simple-rest
Maps Refine resource operations to REST calls against the Express API
Charts
apexcharts + react-apexcharts ^1.4
TotalRevenue (area chart) and PropertyReferrals (horizontal bar chart)
Charts (KPI)
Custom PieChart component
4 donut-style KPI tiles on the dashboard (Properties for Sale/Rent, Customers, Agents)
HTTP
axios ^0.28
All API calls; Axios interceptor attaches Bearer JWT token to every outbound request
Loading State
react-loader-spinner ^5.4
Consistent loading indicator shown across all async pages via the shared Loader component
REST API, CORS, JSON body parsing (50 MB limit for base64 photo uploads)
ODM
mongoose ^6.9
Schemas, validation, populate, and session-based transactions
Media
cloudinary ^1.33
Receives base64-encoded image from client, stores and returns CDN URL
Dev Server
nodemon ^2.0
Auto-restarts Express server on file changes during development
Project evolution & key engineering decisions
Refine the data and the auth layer
Refine’s authProvider interface cleanly separates login, logout, checkAuth, checkError, getPermissions, and getUserIdentity into distinct lifecycle methods. This meant the Google OAuth integration did not need custom redirect handling or session management, Refine’s router integration handles all protected route redirects
The dataProvider from @pankod/refine-simple-rest maps Refine’s resource operations (list, show, create, edit, delete) directly to REST endpoints. Adding a new resource requires only one entry in the resources array in App.tsx, no custom data fetching code needed
useTable was chosen over a manual useState + useEffect approach because it automatically manages pagination state (current page, page size), sorter, and filters as a unified state object, and syncs them to API query params on every change
Mongoose session transactions for data integrity
Create property uses startSession + startTransaction to ensure the Property document and the User.allProperties push happens atomically. If either operation fails, the transaction is rolled back, and neither change persists
Delete property follows the same pattern: the property is removed, and the creator’s allProperties array is updated in the same transaction, preventing orphaned references in either direction
Cloudinary for photo management
The client reads the selected file using the FileReader API into a base64 data URL and includes it directly in the POST/PATCH request body. This avoids multipart form handling on the server
The Express controller calls cloudinary.uploader.upload(photo) with the base64 string. Cloudinary returns a response object; the photo URL is extracted as photoUrl.url and stored in the Property document
On edit, if the user does not change the photo, the existing URL from the database is kept as-is. The upload only runs if a new base64 string is submitted
Idempotent user creation
The POST /api/v1/users endpoint first checks whether a user with the submitted email already exists. If found, it returns the existing document; if not, it creates a new one. This allows the same login flow to be used on both first login and repeat visits without duplicating user records
The userId returned from this endpoint is stored in localStorage alongside the Google profile and is used throughout the app to fetch the agent’s own data
Shared Form and Profile components
The Form component is shared between CreateProperty and EditProperty. A type prop (‘Create’ | ‘Edit’) controls the form title and submit button label. This eliminates duplication while keeping both pages independently routable
The Profile component is shared between AgentProfile and MyProfile. It receives a userId and fetches the user’s data internally. This reduces the surface area for bugs and ensures both profile views render identically
Property type standardisation
Eight property types are hardcoded in the all-properties filter dropdown: Apartment, Villa, Farmhouse, Condos, Townhouse, Chalet, Studio, and Duplex. The same values appear in the create/edit form. This ensures that filter queries always match a valid propertyType in the database, avoiding empty results from typos or free-text input
Project structure
Image: The project folder structure of the client and the server.
Setup & environment
Required environment variables to run Estato locally:
Variable
Location
Description
REACT_APP_GOOGLE_CLIENT_ID
client/.env
Google Cloud Console OAuth 2.0 Client ID
REACT_APP_GOOGLE_CLIENT_SECRET
client/.env
Google Cloud Console OAuth 2.0 Client Secret
REACT_APP_SERVER_URL
client/.env
Backend base URL e.g. http://localhost:8080/api/v1
MONGODB_URL
server/.env
MongoDB Atlas connection string
CLOUDINARY_CLOUD_NAME
server/.env
Cloudinary account cloud name
CLOUDINARY_API_KEY
server/.env
Cloudinary API key
CLOUDINARY_API_SECRET
server/.env
Cloudinary API secret
Image: The Agents page, with agent cards showing listings, revenue and experience.