Where the vendor numbers come from: the nightly aggregation

Every number on **Vendors** traces back to one nightly job. This article is the mechanics companion to [Vendors: your network's spend and procurement leverage, in aggregate](/help/hq-vendors-overvi…

14 min read·Updated July 14, 2026
On this page

What this article covers

Every number on Vendors traces back to one nightly job. This article is the mechanics companion to Vendors: your network's spend and procurement leverage, in aggregate and What HQ sees vs. what stays private: the vendor aggregate boundary: those two explain what each row means and why some vendors don't appear, this one explains how the numbers actually get built, what table they live in, and why a number you know exists on a franchisee's own books can still read "0" or be missing entirely on the page you're looking at right now.

If you've ever wondered why a vendor a franchisee just added this morning isn't on the page yet, or why the median and the network total on the same tile don't seem to reconcile the way you'd expect, this article has the answer.

Where to find it

Open Vendors from the HQ sidebar at hq.verinode.ai/vendors. Every row on that page, the hero, Off Program, Rate Drift, Top Vendors, Renegotiation Candidates, Broadest Network Footprint, and All Vendors, reads from the same underlying rollup table described below. Clicking any vendor tile opens the detail overlay, which reads the same row plus a small amount of additional, on-click-only data covered near the end of this article.

The single source table: the network data

Every number on the Vendors home page, without exception, is read from one table: the network data. It holds exactly one row per canonical vendor per group, that is, one row per distinct vendor entity your network uses, not one row per franchisee-vendor relationship. If six franchisees all use the same equipment rental company, that is still a single row on this table, with the six relationships folded into it.

HQ's page code never reads a franchisee's individual vendor relationship to build this page. It reads this one pre-built table, plus a sibling per-franchisee table described below for the participation list inside a vendor's detail overlay. Both live in the the network layer schema, which is the schema HQ's server is allowed to query directly. The franchisee-level data those two tables are built from lives in pii.*, which HQ's server has no grant to read at all outside the one nightly job that builds the rollup.

The nightly job: hq-aggregate-refresh

A single scheduled cron, hq-aggregate-refresh, is the only bridge from franchisee-level vendor data (your operator data, joined through your operator data) into the network-level table HQ reads (the network data). The Vendors rollup is one of roughly twenty summary tables this same job rebuilds every night, alongside Impact, Programs, Compliance, Facilities, Fleet, and the rest of HQ's network surfaces.

For every franchise group, the job:

  1. 1Reads every your operator data row belonging to a franchisee in that group's directory, and keeps only the ones marked relationship_status = 'active'. A franchisee's paused, ended, or never-activated vendor relationships never contribute to the network numbers.
  2. 2Resolves each relationship's vendor_id to a row in your operator data, which is where a franchisee's own vendor entry carries its canonical_entity_id, the shared identifier that lets Verinode recognize "Acme Equipment Rental" as the same vendor across different franchisees' own bookkeeping, even if two franchisees typed the name slightly differently when they added it.
  3. 3Groups every active relationship across the whole group by that shared canonical_entity_id. This is the step that turns "six franchisees, six private relationships" into "one network row."
  4. 4For each canonical vendor, computes the aggregate figures described below and writes (or updates) one row in the network data, keyed by group and canonical vendor.
  5. 5In the same pass, writes one row per (canonical vendor, franchisee) into the network data, the table that backs the per-franchisee participation list inside a vendor's detail overlay.

The display name on each vendor row comes from Verinode's shared canonical-entity catalog (the benchmark data) when one exists, falling back to whatever name the franchisee's own vendor record carries only when no canonical entity name is on file yet. That's why two franchisees who typed a vendor's name slightly differently still show up as one clean, consistently-named row on your network page.

Note

A franchisee's vendor relationship only ever contributes to this rollup once it's recognized against a canonical entity. Vendors your franchisees have added but that haven't yet been matched to Verinode's shared vendor catalog won't roll up here until that matching completes. This is the same canonical-matching layer that keeps every vendor list on the platform, IQ-side and HQ-side, using one consistent identity per real-world vendor.

What gets computed, column by column

For each canonical vendor, across every active relationship any franchisee in your group holds with it:

  • franchisees_using_count: a simple count of distinct franchisees with an active relationship to this vendor. This is the number the Broadest Network Footprint row sorts on, and it's also the figure the privacy guard (below) checks before a vendor's row is allowed to render.
  • network_monthly_spend_cents: the sum of every franchisee's own recorded monthly spend on this vendor. Only franchisees who have a monthly amount on file contribute; a franchisee using the vendor without a recorded dollar figure still counts toward franchisees_using_count but adds nothing to the spend total.
  • network_annual_spend_cents: simply the monthly total multiplied by twelve. It is not a separately reported figure, so it moves in lockstep with the monthly number.
  • median_monthly_spend_cents: the median, not the average, of the individual franchisees' own monthly spend figures on this vendor. Median is used deliberately, so one franchisee paying an unusually high or low rate doesn't swing the "typical" figure the way an average would.
  • p75_monthly_spend_cents: the 75th-percentile monthly spend across the same set of franchisee figures, that is, the amount that three-quarters of the franchisees using this vendor pay at or below. This is what the Renegotiation Candidates row compares against the median: when the top-quartile figure runs at least twice the median, the vendor is flagged as having unusually wide spend variance across your network.
  • vendor_type_modal: the single most common vendor category recorded across the franchisees using this vendor. If most franchisees categorized it as "Equipment Rental" and one used a different label, "Equipment Rental" wins. This is a mode, not a Verinode-assigned category.
  • avg_satisfaction_rating_x10 and rated_by_franchisees_count: the average of every franchisee's own satisfaction rating for this vendor (stored ×10 internally so the database column stays an integer; the page divides it back down to display a plain X.X figure), alongside how many franchisees actually left a rating. A franchisee using the vendor without ever rating it doesn't pull the average down, they simply aren't counted in either figure.
  • avg_verinode_score_x100 and scored_by_franchisees_count: the same pattern, but for each franchisee's own recorded Verinode Score on the relationship (stored ×100, displayed divided back down).
  • last_relationship_updated_at: the most recent update timestamp across every contributing relationship, letting you tell how current the underlying data is even though the row itself is only ever as fresh as last night's run.

None of these figures are computed live. Every one of them is written once a night by the job above and simply read back, unchanged, whenever the Vendors page loads, until the next run overwrites it.

The per-franchisee table: the network data

Alongside the network summary, the same nightly pass writes one row per (canonical vendor, franchisee) into the network data. This is the table the detail overlay's per-franchisee participation list reads from when you open a specific vendor. Each row carries that franchisee's own monthly spend, billing model and frequency, satisfaction rating, Verinode Score, relationship status, and when it was last updated, the same underlying figures rolled into the summary row above, just not yet aggregated away.

The franchisee's display name is written twice on this row: once in plain text, and once encrypted under your group's own shared encryption key. The page prefers the encrypted copy whenever your group's key is available, and only falls back to the plain-text copy if that decryption can't complete for some reason. Either way, what actually displays to you is subject to the anonymization rule described next, real name or a stable anonymized label, depending on your network's entity model and, for a specific vendor, whether enough franchisees use it to show names at all.

The privacy guard: entity model and cohort size

Two things gate what any of the above actually looks like once it reaches your screen, both read from your group's own settings record, not from anything on the Vendors page itself:

  • Entity model. Every HQ group is configured as either independent operators (the default: each location is a separately owned franchisee) or same entity (a single legal entity operating multiple company-owned locations). This is set once by Verinode at onboarding, not a page toggle.
  • Cohort size. In an independent-operators network, a vendor's row is only ever shown, on the home page or inside its detail overlay, once a sufficient number of distinct franchisees use it. Below that floor, the vendor's row is suppressed entirely rather than risk identifying the one (or too-few) franchisees behind it by elimination, even with names stripped. In a same-entity network this guard doesn't apply at all, since there's no separate business to protect.

This is why the hero's subtext sometimes reads a phrase like "N hidden as single-franchisee," and why an individual vendor's detail overlay sometimes opens straight to an aggregate-only disclosure instead of any figures at all. Both behaviors are the same underlying guard, applied at two different places on the page. The full mechanics, including exactly what that aggregate-only disclosure says and how franchisee names are anonymized when they do show, are covered in the privacy boundary article; this article only needs you to know that the guard exists and that it reads your group's entity model and each vendor's franchisee count before anything else on the page decides what to render.

What's computed on-click instead of overnight

Not everything inside a vendor's detail overlay comes from the nightly rollup. Once you open a vendor that has cleared the cohort guard, a small amount of additional data is read live, at the moment you click, rather than pre-built:

  • The vendor's program membership (which HQ vendor-approval programs it's on, its qualification tier, and any negotiated rate) is read fresh from the network data and the network data on open, not from the nightly rollup. Rate drift, whether the network's median spend on this vendor exceeds its negotiated rate, is also recalculated at that moment using the freshest program terms on file, mirroring the same comparison the home page's Rate Drift row uses.
  • The Verinode Score breakdown, compare alternatives, and deep-dive panel (score history, narratives, category synthesis) are read from Verinode's research layer (the benchmark data and related tables), which is operator-agnostic and updates on its own schedule, independent of your network's nightly sweep entirely.
  • The network team scores section, the per-dimension median built from your own franchisees' private vendor ratings, is computed live on open by reading your operator data.dimension_scores directly, aggregating it down to a median per dimension before anything leaves the function, and applying its own minimum-rater floor per dimension (a dimension with too few raters reads "Insufficient ratings" instead of a number). This is a documented, narrow exception to HQ's usual rule against reading pii.* directly, allowed specifically because the result is aggregated and floor-gated before it's returned. It never runs at all for a vendor that hasn't already cleared the vendor-level cohort guard.

The practical takeaway: the home page's tiles, and the overlay's headline spend and satisfaction figures, are always exactly as fresh as last night's rollup. The program status, rate drift flag, research score, and network team scores inside the overlay can reflect changes made since then, because those pieces are computed at the moment you open the vendor rather than baked into the nightly write.

Why data only appears after the next nightly run

Vendors is built entirely on pre-aggregated tables. There is no code path on this page that reaches into a franchisee's live vendor relationships to answer a page load. That trade is deliberate: computing a network-wide median and percentile across every franchisee's vendor relationships, for every vendor, on every page view, would mean re-running that math for your whole network on every click. Instead, the job runs once overnight, and the page simply reads back whatever it wrote.

The concrete consequence: if a franchisee adds a new vendor relationship, updates their monthly spend, or leaves a satisfaction rating today, none of that reaches the network data or the network data until the next scheduled run completes. Until then, the Vendors page keeps showing last night's numbers, and a genuinely new vendor relationship simply won't appear on the page at all yet, not as a zero, not as an error, just absent, the same way "All Vendors" reads "No vendor relationships rolled up yet. Data appears after the next nightly aggregation." before the first rollup has ever run for a brand-new group.

The schedule itself is a checkpointed sweep, not one single run

hq-aggregate-refresh doesn't run once at midnight and finish. It's scheduled to fire every ten minutes inside a fixed early-morning window, and each invocation picks up a persisted cursor recording which group it last finished, resuming from there rather than starting over. Whichever invocation finishes the last group in the network marks that night's sweep complete; any later invocation inside the same window sees the sweep is already done and does nothing further. The next night, the cursor resets and the sweep works through every group again from the start.

The reason it's spread across a window instead of a single run is scale: rebuilding roughly twenty summary tables, Vendors among them, for every franchisee in every group, is real work, and a large network can have more groups than a single invocation can finish inside its time budget. Splitting the sweep across repeated, resumable runs means a big network's later groups still get processed by the end of the window instead of being silently dropped by a run that ran out of time partway through.

Tip

If a franchisee tells you they just added a vendor relationship or changed their monthly spend and you don't see it on the network page yet, this is almost always why. Give it until the next scheduled sweep completes. There is nothing to refresh or re-trigger from the Vendors page itself, the numbers arrive on their own with the next run.

Common empty and mismatched states, explained

  • A brand-new group's Vendors page is entirely empty, every row reading its own "not yet" sentence. The nightly sweep has never completed a pass for this group. This resolves itself on the next scheduled run once franchisees have active vendor relationships on file, no action needed from HQ.
  • The hero's totals and a specific row's contents don't seem to add up. The hero's headline count and secondary figures are computed across every canonical vendor the network uses, including any currently hidden by the cohort guard. The rows underneath (Top Vendors, Renegotiation Candidates, Broadest Footprint, All Vendors) only ever list vendors that have cleared that guard. A network total that includes a hidden vendor, next to a row list that excludes it, is expected behavior, not a data error.
  • Network monthly spend and median monthly spend look inconsistent for a vendor with several franchisees. The network total is a sum across every franchisee with a recorded monthly figure; the median is the single "typical" franchisee's figure. A vendor with one franchisee paying far more than the rest can show a network total that looks large relative to what the median alone would suggest, that gap is exactly the signal the Renegotiation Candidates row is built to surface.
  • A vendor's satisfaction rating or Verinode Score shows a count lower than its franchisee-using count. Both averages are computed only from franchisees who actually left a rating or had a scored relationship on file. A vendor with, say, five franchisees using it but only three having rated it will show an average built from those three, with the count alongside it making that explicit.
  • Opening a vendor shows different program or rate-drift information than what the home page's Off Program or Rate Drift rows implied. The home page rows are also read fresh at page load (not from the nightly rollup) for program status and negotiated-rate comparisons specifically, so both the home rows and the detail overlay should agree with each other at any given moment, even though the spend and satisfaction figures both display stay pinned to last night's rollup.

Data sources

Data sources

  1. 1.the network data (nightly network rollup, one row per canonical vendor). Your network.
  2. 2.the network data (nightly per-franchisee participation rollup). Your network.
  3. 3.your operator data / your operator data (franchisee-level source data, read only by the nightly job). Your network's franchisees.
  4. 4.the benchmark data (shared vendor identity catalog). Verinode research layer.
  5. 5.the network data / the network data (vendor-approval programs and negotiated rates, read live on click). Your HQ team.
Was this helpful?