Unadhi Guides

FF&E schedule sheet for interior designers and home stagers: product links to SKU, finish, price, stock and a budget per room

Updated 23 Oct 2026 · Disclosure: the product scraper linked below is an Unadhi product on the Apify Store. This guide is free, and the sheet works with a CSV from any tool or with details copied by hand. Current Apify pricing is only on the Store page. This page has no affiliate links and nobody paid to be mentioned.

What you get: one Google Sheet that turns the product links you pick for a client into a furniture, lighting and decor schedule (often called an FF&E schedule): one row per item with room, a thumbnail picture, product name, brand, SKU, the finish or size you chose, its price, whether it is in stock, quantity and line total. A rooms tab adds up each room against its budget, and a Check column tells you, before you order, which items went up in price or out of stock since the client approved them. Formulas are below. The example rows are made up.

The finished sheet

schedule tab (example rows are made up)

RoomPictureItemBrandSKUFinishPriceIn stockQtyLine totalApproved atCheck
Living(thumbnail)Example Linen Sofa 3-seatExample HomeELS-3-OATOatmeal1290TRUE112901290ok to order
Living(thumbnail)Sample Rattan PendantSample Light CoSRP-4545 cm189TRUE2378165price up 24
Bedroom 1(thumbnail)Demo Oak Bedside TableDemo FurnitureDOB-01Natural oak240FALSE2480240out of stock: find alternative
Bedroom 1(thumbnail)Test Wool Rug 160x230Test RugsTWR-160Charcoal310TRUE1310send to client

Who this is for

The slow parts are copying the name, SKU, price and picture from each shop page, and then finding out on order day that a piece went up in price or sold out after the client said yes. This sheet does the copying from a list of links, and the Check column catches the changes.

Step 1: list the picks (tab picks)

Columns: A room, B product link, D qty, E budget note (optional). Paste the link of each product page you are considering, one per row. In C2, make a join key from each link (shop domain plus the last part of the address):

=ARRAYFORMULA(IF(B2:B="",,LOWER(
  REGEXEXTRACT(B2:B, "^(?:https?://)?(?:www\.)?([^/?#]+)") & "|" &
  REGEXEXTRACT(REGEXREPLACE(B2:B, "[?#].*$", ""), "([^/]+)/?$"))))

That turns https://www.example-home.com/collections/sofas/products/linen-sofa-3-seat?variant=123 into example-home.com|linen-sofa-3-seat. The same key is built from the scraper's url field in Step 3, so it does not matter whether you copied the link from a collection page or with a ?variant= on the end.

Step 2: get each product's details (tab scrape)

With the product scraper

Open the E-commerce Product Scraper: Shopify & Woo on the Apify Store, paste column B into Shop, category or product URLs and use this input (JSON tab):

{
  "startUrls": [
    { "url": "https://example-home.com/products/linen-sofa-3-seat" },
    { "url": "https://sample-light.co.uk/products/rattan-pendant" }
  ],
  "maxProductsPerShop": 50,
  "includeDescription": false
}

When the run finishes, export the dataset as CSV, then in your sheet use File → Import → Upload, "Insert new sheet", and name the tab scrape. Keep the headers. Apify flattens lists into numbered columns such as images/0, variants/0/title, variants/0/price, variants/0/sku and variants/0/available; the formulas below find columns by header name, so column order does not matter.

Free, without the scraper

Make a scrape tab with the headers url, title, brand, sku, price, currency, inStock and images/0 (right-click the product photo, "Copy image address"), and type one row per product. Slower, but the same formulas work. Leave the Finish column empty for these rows.

Step 3: the keys tab

Add a small tab named keys. Type key in A1 and this in A2; it builds the same key from the scraper's url column, row for row with the scrape tab:

=ARRAYFORMULA(LET(u, INDEX(scrape!A2:ZZ, 0, MATCH("url", scrape!A1:ZZ1, 0)),
  IF(u="",, LOWER(REGEXEXTRACT(u, "^(?:https?://)?(?:www\.)?([^/?#]+)") & "|" &
  REGEXEXTRACT(REGEXREPLACE(u, "[?#].*$", ""), "([^/]+)/?$")))))

Keeping the key on its own tab means you can replace the scrape tab with a fresh CSV later without losing it. Every lookup in the schedule then uses this one pattern; change only the header in quotes:

=IFERROR(INDEX(scrape!$A:$ZZ,
  MATCH(picks!$C2, keys!$A:$A, 0),
  MATCH("title", scrape!$1:$1, 0)), "")

Use it with title for Item, brand for Brand and currency in a spare column. If a link finds nothing, its shop was probably skipped: see the status table below.

Step 4: the schedule columns

Build the schedule tab with Room =picks!A2 and Qty =picks!D2, and the lookups above.

Finish (F). A Shopify product with several finishes or sizes comes back with each one as a variant: variants/0/title, variants/1/title and so on (for example Oatmeal, Charcoal). The top-level price is the cheapest variant, which may not be the one you chose. Add a column V with the number of the variant you chose (0, 1, 2...; check it against the titles in the scrape tab). Then in F2:

=IF($V2="", "", IFERROR(INDEX(scrape!$A:$ZZ,
  MATCH(picks!$C2, keys!$A:$A, 0),
  MATCH("variants/" & $V2 & "/title", scrape!$1:$1, 0)), ""))

SKU, Price and In stock use the variant's own values when V is set, and the product's otherwise. Price in G2 (swap price for sku to get the SKU, and use available / inStock for stock):

=IFERROR(INDEX(scrape!$A:$ZZ,
  MATCH(picks!$C2, keys!$A:$A, 0),
  MATCH(IF($V2="", "price", "variants/" & $V2 & "/price"), scrape!$1:$1, 0)), "")

Picture in B2 shows the first product photo inside the cell:

=IFERROR(IMAGE(INDEX(scrape!$A:$ZZ,
  MATCH(picks!$C2, keys!$A:$A, 0),
  MATCH("images/0", scrape!$1:$1, 0))), "")

Line total in J2: =IF(G2="", "", G2*I2). If you get a trade discount, add a column Trade % and use =IF(G2="", "", G2*I2*(1-N(W2))). The scraper reads the public price on the shop's page only; it never sees trade prices.

Step 5: the approval price and the Check column

When you send the schedule to the client, copy the Price column and paste it as values only (Ctrl+Shift+V) into Approved at (K). That freezes the price the client said yes to. On order day, run the scraper again with the same links, import the new CSV into the scrape tab with "Replace current sheet", and read the Check column in L2:

=IF(C2="", "",
 IF(H2=FALSE, "out of stock: find alternative",
 IF(K2="", "send to client",
 IF(G2>K2, "price up " & TEXT(G2-K2, "0.##"),
 IF(G2<K2, "price down " & TEXT(K2-G2, "0.##"), "ok to order")))))

Shops list prices in their own currency (the currency column). If your picks come from shops in different currencies, convert them in a spare column before adding totals.

Step 6: the rooms tab

Put each room in A and its budget in B. Then:

C2 (spent):     =SUMIFS(schedule!J:J, schedule!A:A, A2)
D2 (left):      =B2-C2
E2 (items):     =COUNTIFS(schedule!A:A, A2)
F2 (to sort):   =COUNTIFS(schedule!A:A, A2, schedule!L:L, "<>ok to order")

Add a total row with =SUM(B2:B) and =SUM(C2:C). Format D red when it is below zero (Format → Conditional formatting → Less than 0), and the client can see at a glance which room is over budget.

What the shop status values mean

Each run also saves a SHOPS_SUMMARY record (run → Storage → Key-value store) with one line per shop and its status:

statusMeaningWhat to do
okShop readUse the rows
blocked-by-termsThe shop's terms forbid scraping (clause in termsClause)Copy the details by hand
skipped-terms-unverifiableThe terms page could not be read because robots.txt hides itSame
blocked-by-robotsThe shop asks bots not to crawlSame
blocked-by-tos-listA marketplace such as Amazon or EtsySame
no-products-foundNo machine-readable product data (often a JavaScript-only shop)Same
rate-limited, failedToo many requests, or the shop was unreachableRun again later

None of these are charged.

Check before you order

Partner programme opening soon

Partner programme opening soon: join the free waitlist. Planned terms are 50% of each referred sale (75% once you have 5+ paying customers), single level, paid through Gumroad only from revenue received.

Sources

Related

← All guides

Unadhi Guides are free, independent explainers. We correct errors as soon as we find them: write to mininglife7@gmail.com.