Skip to content

Entity Callbacks

Callbacks let you hook into the entity lifecycle to:

  • Transform data before saving (computed fields, slugification)
  • Validate business rules beyond schema validation
  • Trigger side effects after writes (send emails, sync APIs, update caches)
  • Filter/transform data after reading
const articlesCollection: EntityCollection = {
slug: "articles",
callbacks: {
beforeSave: async ({ values, entityId, status }) => {
// Auto-generate slug from title
if (values.title) {
values.slug = values.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
}
// Set timestamps
if (status === "new") {
values.created_at = new Date();
}
values.updated_at = new Date();
return values;
},
afterSave: async ({ values, entityId }) => {
// Send notification
console.log(`Article ${entityId} saved: ${values.title}`);
},
beforeDelete: async ({ entityId }) => {
// Prevent deletion of published articles
// Throw to block the deletion
},
afterRead: async ({ entity }) => {
// Transform data after loading
return entity;
}
},
properties: { /* ... */ }
};

Called before an entity is written to the database. Return the modified values.

beforeSave: async ({
values, // Entity values
entityId, // Entity ID (null for new entities)
status, // "new" | "existing" | "copy"
previousValues, // Previous values (for updates)
context // Full Rebase context
}) => {
// Return modified values
return { ...values, updated_at: new Date() };
}

Throw an error to block the save:

beforeSave: async ({ values }) => {
if (values.price < 0) {
throw new Error("Price cannot be negative");
}
return values;
}

Called after a successful save. Use for side effects.

afterSave: async ({
values, // Saved values
entityId, // Entity ID
previousValues, // Previous values (null for new entities)
status, // "new" | "existing" | "copy"
context
}) => {
// Send webhook
await fetch("https://api.slack.com/webhook", {
method: "POST",
body: JSON.stringify({ text: `New article: ${values.title}` })
});
}

Called when a save operation fails.

afterSaveError: async ({
values,
entityId,
error,
context
}) => {
console.error("Save failed:", error);
}

Called after reading entities from the database. Transform the data for display.

afterRead: async ({
entity, // The entity to transform
context
}) => {
// Add computed fields
return {
...entity,
values: {
...entity.values,
displayName: `${entity.values.first_name} ${entity.values.last_name}`
}
};
}

Called before an entity is deleted. Throw to block deletion.

beforeDelete: async ({
entityId,
entity,
context
}) => {
if (entity.values.status === "published") {
throw new Error("Cannot delete published articles. Unpublish first.");
}
}

Called after a successful deletion.

afterDelete: async ({
entityId,
entity,
context
}) => {
// Cleanup related data
console.log(`Article ${entityId} deleted`);
}

You can also define callbacks at the property level for field-specific transformations:

properties: {
email: {
type: "string",
name: "Email",
callbacks: {
beforeSave: ({ value }) => value?.toLowerCase().trim(),
afterRead: ({ value }) => value // Could decrypt, etc.
}
}
}