GAP School Module 02 — Inventory OS Lesson 2.2

Most WordPress developers building inventory systems think about the admin interface first. What they should think about is: who is actually entering the data, what tool do they already use, and how do we meet them there? For most small businesses, the answer is a spreadsheet — usually Google Sheets.


The situation

Before the sync was built, the Anchor dealership's inventory workflow was: update the spreadsheet (which the entire staff could access and edit), then manually re-enter every change in WordPress admin. Price change? Two places. New unit added? Two places. Unit sold and needs to come down? Two places. That's where the 4-hour inventory update time came from — not from any one change being hard, but from duplicating every change across two systems.

The staff were not going to stop using the spreadsheet. It was their operational system. The solution wasn't to replace it — it was to make WordPress read from it automatically.


What I did

Bound vs. standalone Apps Script

A standalone Google Apps Script lives in its own Google Drive file, runs independently, and must authenticate to any Sheets it needs to access. A bound script lives inside the Google Sheet itself — it's created via Extensions → Apps Script from within the sheet. It gets direct access to the sheet's data without any OAuth complexity, runs in the context of the sheet owner's Google account, and appears under the Extensions menu in the sheet's UI.

For a Sheets-to-WordPress sync, bound is the right choice. The script runs with the sheet's permissions, no service account required, and staff can trigger a sync manually from the Extensions menu if they want an immediate push.

The sync architecture

The script has three core functions: a row parser, a WordPress upsert function, and a scheduler.

Row parser — converts sheet row to a named object
function rowToObject(headers, row) { var obj = {}; headers.forEach(function(header, i) { obj[header] = row[i] !== undefined ? row[i] : ''; }); return obj; } function getInventoryData() { var sheet = SpreadsheetApp.getActiveSpreadsheet() .getSheetByName('Inventory'); var data = sheet.getDataRange().getValues(); var headers = data[0].map(function(h) { return h.toString().trim().toLowerCase().replace(/\s+/g, '_'); }); return data.slice(1) .filter(function(row) { return row[0] !== ''; }) .map(function(row) { return rowToObject(headers, row); }); }
WordPress upsert — PUT if post exists, POST if new
function upsertToWordPress(unit) { var props = PropertiesService.getScriptProperties(); var baseUrl = props.getProperty('WP_BASE_URL'); var auth = getAuthHeader(); // Search for existing post by stock number var searchUrl = baseUrl + '/wp-json/wp/v2/[client]_inventory' + '?meta_key=[client]_stock_number' + '&meta_value=' + encodeURIComponent(unit.stock_number) + '&per_page=1'; var searchResp = UrlFetchApp.fetch(searchUrl, { headers: { 'Authorization': auth }, muteHttpExceptions: true }); var existing = JSON.parse(searchResp.getContentText()); var postId = (existing.length > 0) ? existing[0].id : null; var payload = { title: unit.year + ' ' + unit.make + ' ' + unit.model, status: unit.status === 'sold' ? 'draft' : 'publish', meta: { '[client]_price': parseFloat(unit.price) || 0, '[client]_stock_number': unit.stock_number, '[client]_year': parseInt(unit.year) || 0, '[client]_make': unit.make, '[client]_model': unit.model, '[client]_condition': unit.condition, '[client]_sort_priority': parseInt(unit.sort_priority) || 0 } }; var method = postId ? 'put' : 'post'; var url = postId ? baseUrl + '/wp-json/wp/v2/[client]_inventory/' + postId : baseUrl + '/wp-json/wp/v2/[client]_inventory'; UrlFetchApp.fetch(url, { method: method, headers: { 'Authorization': auth, 'Content-Type': 'application/json' }, payload: JSON.stringify(payload), muteHttpExceptions: true }); }

WordPress Application Passwords for auth

WordPress Application Passwords (built in since WP 5.6) create a separate credential for API access that isn't the admin account password. Generate one at Users → Profile → Application Passwords. The credential is Base64-encoded as username:app_password and sent as a Bearer token.

Auth header — credentials stored in Script Properties, never in code
function getAuthHeader() { var props = PropertiesService.getScriptProperties(); var username = props.getProperty('WP_USERNAME'); var appPass = props.getProperty('WP_APP_PASSWORD'); return 'Basic ' + Utilities.base64Encode(username + ':' + appPass); }

Credentials go in Script Properties (Extensions → Apps Script → Project Settings → Script Properties), never hardcoded. A hardcoded credential in a bound script is readable by anyone with edit access to the sheet.

Version control with clasp

Google Apps Script has its own IDE, but it's not under version control by default. clasp (Command Line Apps Script) pulls the bound script to your local filesystem and pushes changes back. The script is then in your git repository like any other code.

clasp workflow
# Install and authenticate npx -y @google/clasp@latest login --no-localhost # Pull existing bound script npx clasp clone <SCRIPT_ID> # Push local changes back to the bound script npx clasp push

Why it matters

The spreadsheet was already the operational system. The staff knew how to use it, had their own column order preferences, and were updating it daily. The sync doesn't change their workflow at all — they update the sheet exactly as before. WordPress reflects those changes on the next sync run (triggered by a time-based Apps Script trigger, set to run every hour or twice daily).

This is what produces the 60× speedup. Not faster typing — eliminating the duplicate entry entirely. The staff does the work once, in the tool they already use, and the website updates itself.


The Anchor build

The sync script has been running in production since Era 1. It runs twice daily on a time-based trigger. When a unit's price changes in the sheet, the website price reflects it within 12 hours — without anyone touching WordPress admin. When a unit sells and the sheet marks it sold, the listing automatically moves to draft (no longer publicly visible) on the next sync.

In the first month after deployment, inventory update time dropped from roughly 4 hours per session to about 4 minutes — the time it takes to update the spreadsheet. The 60× speedup comes entirely from eliminating the duplication step.


Do this, not that

  • Use a bound script, not standalone. Bound scripts access the sheet directly with no extra auth configuration. Standalone scripts require a service account or OAuth flow for the same result.
  • Store credentials in Script Properties, never in code. Script Properties are encrypted at rest. Code is readable by anyone with sheet edit access.
  • Use WordPress Application Passwords, not admin passwords. Application Passwords are revocable independently. Rotating them doesn't lock you out of WordPress admin.
  • Use clasp for version control. A script that lives only in the Google Apps Script IDE is a script that can't be code-reviewed, diffed, or rolled back. Pull it local, commit it, treat it like any other code.
  • Use muteHttpExceptions: true on every UrlFetchApp call. Without it, any non-2xx response throws an uncaught exception that terminates the sync mid-run and leaves inventory in a partial state.
When you’re ready to build

The lessons are yours. When you want it built, we’re here.

Every lesson stays free — no account, no paywall, no email gate, ever. But if you’d rather have this system standing on your business than wire all 48 lessons yourself, leave your email. We’ll send you a direct line to a build — and you’ll be first to hear when we add new tools to the curriculum.

None of this gates a single lesson. The curriculum was free before you got here and it stays that way.

We’ll use your email to send you a fast-track to a GAP build and occasional notes on how GAP builds digital sales departments. Lessons stay 100% free — no email required to read any of them. We never share or sell your information. Unsubscribe any time. Privacy policy at gapindustriesllc.com/privacy.html.

Done learning how it’s built? We’ll build it.

You came here to understand the system, and now you do. If you’d rather have it standing on your business than spend the next three months wiring it yourself, GAP Concierge is the same architecture from these lessons — a white-label AI agent that knows your catalog and captures your leads — set up for you, from $97/mo.

See GAP Concierge →