Skip to content
Gravity Tables
tutorial

Build an inventory tracker with Gravity Forms and Gravity Tables

A complete pattern for tracking stock on a WordPress site. Public catalog, staff workspace, low-stock alerts, multi-location split, restock workflow, all from one Gravity Form, no inventory SaaS subscription.

· 8 min read · By Fahd Murtaza

Inventory SaaS sits at $50-$300 per month per location. For a single-warehouse small business that already runs WordPress for the storefront, that’s pure overhead, the data lives in your forms, your team logs into your site daily, and the operations are simple enough that a real spreadsheet would handle them.

This guide ships the inventory pattern most small operations actually need: catalog browsing, staff stock workspace, low-stock alerts, restock workflow, and multi-location split when growth requires it. One Gravity Form, one shortcode, no SaaS bill.

What you’ll build#

Three URLs:

  • /catalog, public-facing product catalog with search and category filters
  • /staff/stock, staff stock workspace (the edit surface)
  • /staff/restocks, restock-pending view (filtered subset of /staff/stock)

Plus a daily cron that emails the operations lead when any item drops below its reorder point.

Step 1: shape the form#

Customer-visible fields (these power the public catalog):

  • name, single-line text, required
  • sku, single-line text, required, unique
  • description, paragraph
  • category, dropdown
  • price, number/currency
  • image_url, URL or file upload

Hidden admin-side fields:

  • qty_on_hand, number, default 0. Current stock count.
  • reorder_point, number, default 5. Below this → alert.
  • location, single-line text, default empty. For multi-warehouse setups.
  • supplier, single-line text, default empty.
  • status, Hidden, default active. Values: active, discontinued, pending_restock.
  • last_counted, Date, default empty. Set when staff confirm a count.

Step 2: build the public catalog#

A read-only gravity_table shortcode at /catalog:

[gravity_table id="inventory"
  columns="image_url,name,sku,category,price,qty_on_hand"
  filter="status:active"
  filters="category"
  search_field="name,sku,description"
  per_page="24"
  sort="name:asc"
  mobile_layout="cards"
  allow_edit=""]

Why these knobs:

  • filter="status:active", discontinued or pending-restock items don’t appear. Keeps the public catalog clean automatically.
  • search_field, search hits name, sku, and description so customers find what they’re looking for whether they remember the model name, the SKU, or just a feature
  • per_page="24", divisible by 1/2/3/4 columns; renders cleanly on every viewport
  • mobile_layout="cards", phone customers get one card per row with the image prominent
  • allow_edit="", explicitly read-only. Customers don’t edit anything; the public role gate is implicit (no allowed_roles means everyone sees it).

If you want each row to link to a product detail page, add a [lookup_field] column or use Gravity Tables’ built-in link_field parameter to point each row at the matching post.

Step 3: build the staff stock workspace#

/staff/stock is where the team manages inventory. Different shortcode, same form:

[gravity_table id="inventory"
  columns="image_url,name,sku,location,qty_on_hand,reorder_point,status,last_counted,supplier"
  allowed_roles="warehouse-staff,warehouse-lead,administrator"
  allow_edit="qty_on_hand,location,status,last_counted,supplier"
  edit_permissions="status:warehouse-lead,reorder_point:warehouse-lead"
  filters="category,location,status"
  bulk="mark_restock,export"
  audit_log="true"
  auto_refresh="true"
  refresh_interval="60"
  sort="qty_on_hand:asc"]

Five intentional choices:

  • sort="qty_on_hand:asc", items closest to running out float to the top of the workspace by default. Staff see the priority queue without applying a filter.
  • edit_permissions, staff edit qty, location, last-counted, supplier. Only warehouse-lead can change status (so an item doesn’t get accidentally marked discontinued) or reorder_point (a parameter-tuning decision, not a daily-ops decision).
  • auto_refresh="60", when one staffer counts a shelf and updates qty, the other staff workspace tabs refresh within a minute. Prevents two people counting the same shelf.
  • bulk="mark_restock,export", mark_restock is a custom action that flips status to pending_restock and triggers a notification. Export gives the lead an Excel snapshot for monthly reconciliation.
  • audit_log="true", every qty change recorded with user + timestamp. When the year-end count doesn’t match expected, the audit log tells you exactly when discrepancies were introduced.

Step 4: register the mark_restock bulk action#

In your child theme’s functions.php:

add_filter('gt_register_bulk_actions', function ($actions) {
    $actions['mark_restock'] = [
        'label' => 'Mark for restock',
        'capability' => 'edit_others_posts',
        'callback' => function ($entry_ids) {
            foreach ($entry_ids as $id) {
                GFAPI::update_entry_field($id, 'status', 'pending_restock');
            }
            // Notify the warehouse lead with a single batched email
            $count = count($entry_ids);
            wp_mail(
                'warehouse-lead@your-co.com',
                "{$count} items marked for restock",
                "View at /staff/restocks"
            );
            return ['success' => $count, 'message' => "Marked {$count} for restock"];
        },
    ];
    return $actions;
});

The custom action bundles two operations: flipping the status field on each selected entry, plus a single batched notification email. Staff don’t need to email the lead individually.

Step 5: build the restocks view#

The pending-restock workspace at /staff/restocks is just /staff/stock with a different filter:

[gravity_table id="inventory"
  columns="name,sku,supplier,qty_on_hand,reorder_point,location"
  filter="status:pending_restock"
  allowed_roles="warehouse-lead,administrator"
  allow_edit="qty_on_hand,status"
  bulk="confirm_restocked,cancel_restock"
  audit_log="true"
  sort="supplier:asc,name:asc"]

Two custom bulk actions for the lead’s workflow:

$actions['confirm_restocked'] = [
    'label' => 'Confirm received',
    'capability' => 'edit_others_posts',
    'prompt' => [
        'type' => 'number',
        'label' => 'Quantity received (will be added to current qty_on_hand):',
    ],
    'callback' => function ($entry_ids, $args) {
        $delta = max(0, intval($args['number'] ?? 0));
        foreach ($entry_ids as $id) {
            $current = intval(gform_get_meta($id, 'qty_on_hand'));
            GFAPI::update_entry_field($id, 'qty_on_hand', $current + $delta);
            GFAPI::update_entry_field($id, 'status', 'active');
            GFAPI::update_entry_field($id, 'last_counted', wp_date('Y-m-d'));
        }
        return ['success' => count($entry_ids), 'message' => 'Updated'];
    },
];

$actions['cancel_restock'] = [
    'label' => 'Cancel restock (back to active)',
    'capability' => 'edit_others_posts',
    'callback' => function ($entry_ids) {
        foreach ($entry_ids as $id) {
            GFAPI::update_entry_field($id, 'status', 'active');
        }
        return ['success' => count($entry_ids), 'message' => 'Cancelled'];
    },
];

Notable: the confirm_restocked action takes a numeric prompt for “how many were received” and adds that to the existing qty_on_hand. Sets last_counted to today as a side effect.

Step 6: low-stock alert via daily cron#

A daily WP-Cron task that emails the operations lead when any item is below its reorder point:

add_action('init', function () {
    if (!wp_next_scheduled('gt_low_stock_check')) {
        wp_schedule_event(strtotime('tomorrow 8am'), 'daily', 'gt_low_stock_check');
    }
});

add_action('gt_low_stock_check', function () {
    // Get all active items
    $items = GFAPI::get_entries('inventory', [
        'field_filters' => [
            ['key' => 'status', 'value' => 'active'],
        ],
    ]);

    $low = [];
    foreach ($items as $item) {
        $qty = intval(rgar($item, 'qty_on_hand'));
        $reorder = intval(rgar($item, 'reorder_point'));
        if ($reorder > 0 && $qty < $reorder) {
            $low[] = [
                'name' => rgar($item, 'name'),
                'sku' => rgar($item, 'sku'),
                'qty' => $qty,
                'reorder' => $reorder,
            ];
        }
    }

    if (empty($low)) return;

    $body = "These items are below their reorder point:\n\n";
    foreach ($low as $i) {
        $body .= "{$i['name']} ({$i['sku']}): {$i['qty']} on hand, reorder at {$i['reorder']}\n";
    }
    $body .= "\nMark for restock at /staff/stock?gt_filter_status=active&gt_sort=qty_on_hand:asc";

    wp_mail('warehouse-lead@your-co.com', count($low) . ' items below reorder point', $body);
});

Daily was deliberate. Hourly is too noisy for inventory ops; weekly misses fast-moving items. The 8 AM cadence puts the email in the lead’s inbox before they start the day.

Step 7: multi-location split#

If you grow past one warehouse, the location field becomes the partition key. Two changes:

Per-location view URLs#

<nav aria-label="Stock by location">
  <a href="/staff/stock">All locations</a>
  <a href="/staff/stock?gt_filter_location=warehouse-east">East</a>
  <a href="/staff/stock?gt_filter_location=warehouse-west">West</a>
  <a href="/staff/stock?gt_filter_location=storefront-1">Storefront 1</a>
</nav>

Per-location staff scoping#

If East-warehouse staff should only edit East-warehouse items, use filter_user_owns="location" after first populating each user’s profile with their assigned location (custom user meta key). Or use a custom hook on gt_filter_query to scope by current_user_can('manage_location_east') capability.

Capacity and dashboard reporting#

For the operations lead, a separate gravity_table shortcode at /staff/dashboard with grouped totals:

[gravity_table id="inventory"
  columns="category,_count,_sum_qty_on_hand"
  group_by="category"
  totals="_sum_qty_on_hand"
  filter="status:active"
  allowed_roles="warehouse-lead,administrator"
  allow_edit=""
  per_page="50"]

One row per category, with item count and total quantity. Compare to your sales velocity (a separate Sales form, ideally) and decide whether to reorder more aggressively per category.

What this gives you#

For a 1-500 SKU operation:

  • No SaaS subscription, replace $50-$300/mo per location with your existing WP hosting cost
  • Public catalog and staff workspace from one form, no integration glue
  • Audit log of every qty change, useful when the year-end count doesn’t reconcile
  • Low-stock alerts that reach the right person, on a daily cadence that doesn’t become noise
  • Multi-location ready when you grow

What it doesn’t give you#

  • Barcode scanning, staff manually type SKUs or use a barcode-to-keyboard scanner that types into the search box (works fine for our pattern, but is not a “scan and update” workflow)
  • PO management, purchase orders, vendor invoicing, payment tracking. Use Gravity Forms with a separate “po” form, or integrate with QuickBooks via a separate tool.
  • Forecasting, qty-needed-next-month projections. Export to a spreadsheet for that.
  • Multi-currency, single-currency setups only

For most small operations, this is the right level of tool. Past 5,000 SKUs and 5+ locations, a dedicated inventory system becomes worth the cost.

Recipe variations#

Library / equipment lending#

qty_on_hand becomes “available copies”; add a separate “checkouts” form linking borrower + item + due date. The catalog shows availability; the staff workspace tracks overdue items via a date filter.

Restaurant supply#

Categories: produce, dry goods, dairy, beverages. Reorder_point varies by category (perishables higher, dry goods lower). Daily cron becomes 6 AM so the kitchen’s morning order goes out with breakfast prep.

Photography / film equipment rental#

Add serial_number, condition, and next_service_due fields. Staff workspace filters by condition; the dashboard groups by category and surfaces items overdue for service.

Don’ts#

Don’t expose qty_on_hand as user-editable on the public form#

Hidden admin field only. Customers should never set their own quantity; staff verifies through counts.

Don’t skip the last_counted field#

Stock counts drift. Recording when each item was last manually verified means staff (and the audit log) can identify the items that haven’t been counted recently and prioritise them.

Don’t run low-stock alerts more than daily#

Hourly polling generates noise. Items dropping below reorder midweek is fine to flag once, not 24 times.