Hooks allow you to execute custom logic at specific points in the document lifecycle, enabling validation, transformation, and integration with external systems.
Overview
Permission Mongo supports:
Pre-operation hooks - Execute before create, update, or delete
Post-operation hooks - Execute after operations complete
Multiple hook types - Validation, field setting, transformations, HTTP webhooks
Conditional execution - Run hooks based on conditions
Async webhooks - Non-blocking HTTP calls for integrations
Hook events
Hooks are triggered at these lifecycle points:
Before document creation. Can modify the document, validate, or call webhooks.
After document is created. Useful for notifications and external system updates.
Before document update. Can validate changes, transform fields, or prevent updates.
After document is updated. Includes change information.
Before document deletion. Can prevent deletion or log the operation.
After document is deleted. Useful for cleanup and notifications.
const (
HookPreCreate HookEventType = "pre_create"
HookPostCreate HookEventType = "post_create"
HookPreUpdate HookEventType = "pre_update"
HookPostUpdate HookEventType = "post_update"
HookPreDelete HookEventType = "pre_delete"
HookPostDelete HookEventType = "post_delete"
)
Hook actions
Each hook can perform one or more actions:
Set field
Automatically set field values:
collections :
documents :
pre_create :
- action : set_field
field : created_at
value : $now
- action : set_field
field : owner_id
value : $user.id
- action : set_field
field : tenant_id
value : $user.tenant_id
// executeSetField sets a field value on the document
func ( e * Executor ) executeSetField ( doc map [ string ] interface {}, action * config . HookAction , hookCtx * HookContext ) error {
if action . Field == "" || action . Value == "" {
return fmt . Errorf ( "set_field requires field and value" )
}
// Substitute variables in the value
value := SubstituteVariables ( action . Value , hookCtx )
// Set the field (supports nested fields with dot notation)
setNestedField ( doc , action . Field , value )
return nil
}
Validate
Add custom validation rules:
collections :
documents :
pre_create :
- action : validate
condition : "$doc.status == 'published'"
when : "len($doc.content) == 0"
error : "Published documents must have content"
- action : validate
condition : "$doc.priority > 0 && $doc.priority <= 5"
error : "Priority must be between 1 and 5"
Expression that must evaluate to true for validation to pass. If false, the hook fails with the error message.
Guard condition - only run this validation if the condition is true.
Error message returned if validation fails.
Validate reference
Ensure referenced documents exist:
collections :
documents :
pre_create :
- action : validate_ref
field : customer_id
collection : customers
error : "Customer not found"
- action : validate_ref
field : project_id
collection : projects
optional : true # Only validate if field is present
error : "Project not found"
// executeValidateRef validates that a referenced document exists
func ( e * Executor ) executeValidateRef ( ctx context . Context , doc map [ string ] interface {}, action * config . HookAction ) error {
if action . Field == "" || action . Collection == "" {
return fmt . Errorf ( "validate_ref requires field and collection" )
}
// Get the reference value from the document
refValue := getNestedField ( doc , action . Field )
if refValue == nil {
if action . Optional {
return nil
}
return fmt . Errorf ( " %w : field %s is empty" , ErrRefNotFound , action . Field )
}
refID , ok := refValue .( string )
if ! ok {
return fmt . Errorf ( "reference field %s is not a string" , action . Field )
}
// Check if the referenced document exists
var filter bson . M
if oid , err := primitive . ObjectIDFromHex ( refID ); err == nil {
filter = bson . M { "_id" : oid }
} else {
filter = bson . M { "_id" : refID }
}
exists , err := e . store . Exists ( ctx , action . Collection , filter )
if err != nil {
return fmt . Errorf ( "failed to check reference: %w " , err )
}
if ! exists {
return fmt . Errorf ( " %w : %s " , ErrRefNotFound , action . Error )
}
return nil
}
Apply transformations to field values:
collections :
users :
pre_create :
- action : transform
field : email
transform : lowercase
- action : transform
field : username
transform : trim
pre_update :
- action : transform
field : name
transform : trim
Supported transformations:
lowercase - Convert to lowercase
uppercase - Convert to uppercase
trim - Remove leading/trailing whitespace
// executeTransform applies a transformation to a field
func ( e * Executor ) executeTransform ( doc map [ string ] interface {}, action * config . HookAction ) error {
if action . Field == "" || action . Transform == "" {
return fmt . Errorf ( "transform requires field and transform type" )
}
value := getNestedField ( doc , action . Field )
if value == nil {
return nil
}
strValue , ok := value .( string )
if ! ok {
return nil
}
var transformed string
switch config . TransformType ( action . Transform ) {
case config . TransformLowercase :
transformed = strings . ToLower ( strValue )
case config . TransformUppercase :
transformed = strings . ToUpper ( strValue )
case config . TransformTrim :
transformed = strings . TrimSpace ( strValue )
default :
return fmt . Errorf ( "unknown transform type: %s " , action . Transform )
}
setNestedField ( doc , action . Field , transformed )
return nil
}
HTTP webhook
Call external HTTP endpoints:
collections :
orders :
post_create :
- action : http
url : https://api.example.com/webhooks/order-created
method : POST
headers :
Authorization : "Bearer ${WEBHOOK_TOKEN}"
X-Tenant-ID : $user.tenant_id
body :
order_id : $doc._id
customer_id : $doc.customer_id
total : $doc.total
created_by : $user.id
async : true
timeout : 5000
retry_count : 3
on_error : log
HTTP method: GET, POST, PUT, PATCH, DELETE
HTTP headers to send. Supports variable substitution.
JSON body to send. Supports variable substitution.
If true, execute webhook asynchronously without blocking the operation
Number of retry attempts on failure
Error handling: fail (abort operation), log (log and continue), ignore (silent failure)
// executeHTTP makes an HTTP webhook call
func ( e * Executor ) executeHTTP ( ctx context . Context , action * config . HookAction , hookCtx * HookContext ) error {
if action . IsAsync () {
// Execute asynchronously
go func () {
if err := e . doHTTPRequest ( context . Background (), action , hookCtx ); err != nil {
switch action . GetOnError () {
case config . OnErrorLog :
logging . Error ( "Async HTTP hook failed" ,
slog . String ( "url" , action . URL ),
slog . String ( "error" , err . Error ()),
)
case config . OnErrorIgnore :
// Do nothing
}
}
}()
return nil
}
// Execute synchronously
return e . doHTTPRequest ( ctx , action , hookCtx )
}
Variable substitution
Hooks support variable substitution in value, url, headers, and body fields:
Document variables
$doc.field_name # Current document field
$doc.nested.field # Nested field with dot notation
$old.field_name # Previous value (in pre_update/post_update)
$new.field_name # New value (in post_update)
User variables
$user.id # User ID
$user.tenant_id # Tenant ID
$user.roles # Array of user roles
$user.claims.custom # Custom JWT claim
System variables
$now # Current timestamp
$changes # Array of field changes (in post_update)
// resolveVariable resolves a variable expression
func resolveVariable ( expr string , hookCtx * HookContext ) interface {} {
expr = strings . TrimSpace ( expr )
if ! strings . HasPrefix ( expr , "$" ) {
return nil
}
expr = expr [ 1 :]
parts := strings . Split ( expr , "." )
var root interface {}
rootName := parts [ 0 ]
switch rootName {
case "doc" :
root = hookCtx . Doc
case "old" :
root = hookCtx . Old
case "new" :
root = hookCtx . New
case "user" :
root = userToMap ( hookCtx . User )
case "now" :
return hookCtx . Now
case "changes" :
return changesToSlice ( hookCtx . Changes )
}
// Navigate nested fields
if len ( parts ) == 1 {
return root
}
if rootMap , ok := root .( map [ string ] interface {}); ok {
return getNestedField ( rootMap , strings . Join ( parts [ 1 :], "." ))
}
return nil
}
Conditional execution
Run hooks conditionally with when clauses:
collections :
documents :
pre_update :
# Only set published_at when status changes to published
- action : set_field
field : published_at
value : $now
when : "$new.status == 'published' && $old.status != 'published'"
# Validate only for high-priority documents
- action : validate
condition : "len($doc.content) > 100"
when : "$doc.priority >= 4"
error : "High-priority documents need detailed content"
post_update :
# Send webhook only when status changes
- action : http
url : https://api.example.com/webhooks/status-changed
when : "$old.status != $new.status"
async : true
Condition expressions support:
Comparison: ==, !=, >, <, >=, <=
Logical: &&, ||, !
Membership: in, not in
Functions: len()
Hook execution order
Hooks execute in the order defined:
Pre-operation hooks run first, in definition order
Schema validation runs after pre-operation hooks
RBAC authorization checks permissions
Database operation executes (create/update/delete)
Post-operation hooks run last, in definition order
If any synchronous hook fails, the operation is aborted.
// ExecuteHooks runs all hooks for an event
func ( e * Executor ) ExecuteHooks ( ctx context . Context , opts * ExecuteOpts ) ( * HookResult , error ) {
collHooks := e . hooks . GetCollectionHooks ( opts . Collection )
if collHooks == nil {
return & HookResult { Document : opts . Document }, nil
}
actions := collHooks . GetHooks ( opts . Event )
if len ( actions ) == 0 {
return & HookResult { Document : opts . Document }, nil
}
hookCtx := & HookContext {
Doc : copyMap ( opts . Document ),
Old : opts . OldDoc ,
New : opts . NewDoc ,
User : opts . User ,
Now : time . Now (). UTC (),
Changes : opts . Changes ,
}
result := & HookResult {
Document : hookCtx . Doc ,
Errors : [] HookError {},
}
// Execute each action in order
for _ , action := range actions {
// Check guard condition
if action . GetCondition () != "" {
conditionMet , err := e . evaluateCondition ( action . GetCondition (), hookCtx )
if err != nil || ! conditionMet {
continue
}
}
// Execute the action
var actionErr error
switch config . ActionType ( action . Action ) {
case config . ActionSetField :
actionErr = e . executeSetField ( hookCtx . Doc , action , hookCtx )
case config . ActionValidate :
actionErr = e . executeValidate ( hookCtx . Doc , action , hookCtx )
case config . ActionValidateRef :
actionErr = e . executeValidateRef ( ctx , hookCtx . Doc , action )
case config . ActionTransform :
actionErr = e . executeTransform ( hookCtx . Doc , action )
case config . ActionHTTP :
actionErr = e . executeHTTP ( ctx , action , hookCtx )
}
if actionErr != nil {
result . Errors = append ( result . Errors , HookError {
Action : action . Action ,
Field : action . Field ,
Message : actionErr . Error (),
})
// For validation errors, abort if not async
if ! action . IsAsync () {
result . Aborted = true
break
}
}
}
result . Document = hookCtx . Doc
return result , nil
}
Use cases
Automatic field population
Use set_field in pre_create to automatically set created_at, owner_id, tenant_id, and other metadata fields.
Business logic validation
Add validate hooks for complex business rules that go beyond schema validation, like checking date ranges or cross-field dependencies.
Apply transform actions to normalize data (lowercase emails, trim whitespace) before storage.
External system integration
Use http webhooks in post_create and post_update to notify external systems of changes.
Validate foreign keys with validate_ref to ensure referenced documents exist before creating relationships.
Add custom metadata in pre_create/pre_update hooks to enrich audit logs with business context.
Best practices
Use async webhooks for non-critical operations
Set async: true for webhooks that shouldn’t block the main operation. Handle failures with on_error: log.
Hooks should perform focused, single-purpose actions. Complex logic belongs in external services called via HTTP hooks.
Set retry_count: 3 for webhooks to handle transient failures automatically.
Use meaningful error messages
Provide clear, actionable error messages in validation hooks to help users fix issues.
Hooks run on every operation - test them with various document states and edge cases.