Wix Velo data hooks explained: run code when your data changes
Mosaddik Billah 4 min read

On this page
A data hook is a backend function that Wix runs automatically before or after something happens to an item in a CMS collection: an item is added, updated, removed or read. Hooks are the right place for rules that must always apply, whichever page or form the data comes from. If you check a value only in page code, a visitor can get around it; a hook runs on the server, every time.
Where hooks live#
Hooks go in a backend file called data.js. In the Velo sidebar, open the Backend section and add a data.js file (or use the collection’s menu in the CMS to add a hook, which creates the file for you).
Each hook is an exported function named after the collection’s ID and the event:
// backend/data.js
export function Leads_beforeInsert(item, context) {
// runs before a new item is saved to the "Leads" collection
return item;
}Leads is the collection ID, exactly as it appears in the CMS, including capital letters. If the name doesn’t match, the hook silently never runs.
The hooks you’ll use most#
| Hook | Runs | Typical use |
|---|---|---|
beforeInsert |
Before a new item is saved | Clean up or fill in fields, reject bad data |
afterInsert |
After a new item is saved | Notify another system, log the entry |
beforeUpdate |
Before an item is changed | Protect fields, keep values consistent |
afterUpdate |
After an item is changed | React to a status change |
beforeRemove |
Before an item is deleted | Block deletes that would break something |
afterQuery |
For each item read | Add a calculated field to what the page receives |
Every hook gets the item (or, for beforeRemove, the item’s ID) and a context object with details such as collectionName, userId and userRole.
Example 1: clean up form data before it’s saved#
export function Leads_beforeInsert(item, context) {
item.email = (item.email || "").trim().toLowerCase();
item.name = (item.name || "").trim();
item.status = "new";
return item;
}Every lead now has a tidy email address and a starting status, whether it came from a Wix form, a custom form or an import through code.
Always return the item. A before hook must return the item (or a promise of it). If you forget, the save doesn’t get the data you expect.
Example 2: reject bad data#
To stop an operation, reject it. The page code that tried to save gets the error, and nothing is saved.
export function Leads_beforeInsert(item, context) {
const email = (item.email || "").trim().toLowerCase();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return Promise.reject(new Error("Please enter a valid email address."));
}
item.email = email;
return item;
}This is the server-side half of form validation. Checks in the page give visitors quick feedback, as in how to validate a Wix form with Velo; the hook makes sure the rule holds even if the page check is bypassed.
Example 3: protect a field from being changed#
import wixData from "wix-data";
export async function Orders_beforeUpdate(item, context) {
const current = await wixData.get("Orders", item._id, { suppressAuth: true });
if (current && current.orderNumber !== item.orderNumber) {
item.orderNumber = current.orderNumber; // keep the original
}
return item;
}Hooks can be async, so they can read other data before deciding. Use suppressAuth only in backend code like this, where you control exactly what happens with the result.
Example 4: tell another system about a new item#
import { fetch } from "wix-fetch";
import { getSecret } from "wix-secrets-backend";
export async function Leads_afterInsert(item, context) {
try {
const key = await getSecret("CRM_API_KEY");
await fetch("https://api.example-crm.com/contacts", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
body: JSON.stringify({ email: item.email, name: item.name, source: "website" }),
});
} catch (err) {
console.error("CRM sync failed", err);
}
return item;
}The try...catch matters: the lead is already saved, so a CRM outage should be logged, not turned into an error for the visitor. The API key stays in the Secrets Manager, as covered in how to call an external API from Wix Velo. For a complete example of this pattern, see sending Wix form submissions to Google Sheets.
Things to watch#
- Hooks run on every matching operation, including imports and code that updates many items at once. Keep them quick, and avoid slow external calls in
beforehooks. - Don’t update the same collection inside its own
afterUpdatehook without a guard. Each update fires the hook again, and you can end up in a loop. - Skipping hooks on purpose: backend code can pass
{ suppressHooks: true }in the options of awixDatacall, for example in a one-off data migration. - Test with Logs.
console.logoutput from hooks shows in the site’s Logs in the dashboard, which is the quickest way to see whether a hook ran and what it received.
If you need rules like these built into your Wix site’s data, I do Wix Velo development.
Comments