Overview
The hooks configuration file (hooks.yml) defines lifecycle hooks that execute before or after document operations. Hooks enable validation, field transformations, reference validation, and integration with external systems via HTTP webhooks.
Configuration Structure
version: "1.0"
hooks:
<collection_name>:
pre_create: []
post_create: []
pre_update: []
post_update: []
pre_delete: []
post_delete: []
Hook Types
Each collection can define hooks for six lifecycle events:
Execute before creating a new document. Can modify the document or reject creation.
Execute after creating a new document. Document is already saved.
Execute before updating a document. Can modify changes or reject update.
Execute after updating a document. Changes are already saved.
Execute before deleting a document. Can prevent deletion.
Execute after deleting a document. Document is already deleted.
Hook Actions
Each hook is an object with an action field specifying the operation type.
set_field
Set a field value using an expression.
Name of the field to set.
Expression evaluating to the field value. Supports context variables.
Optional condition expression. Hook only executes if condition evaluates to true.
Example
pre_create:
- action: set_field
field: tenant_id
value: $user.tenant_id
- action: set_field
field: order_number
value: $auto.sequence.orders
validate
Validate a condition and reject the operation if it fails.
Expression that must evaluate to true for validation to pass.
Error message returned if validation fails.
Example
pre_update:
- action: validate
condition: $new.manager_id != $doc._id
error: "User cannot be their own manager"
- action: validate
condition: $doc.cost == null || $doc.cost <= $doc.price
error: "Cost cannot exceed price"
validate_ref
Validate that a referenced document exists.
Field containing the reference ID.
Name of the referenced collection.
Whether the reference is optional (null allowed).
Custom error message if reference is not found.
Example
pre_create:
- action: validate_ref
field: manager_id
collection: users
optional: true
error: "Manager not found"
- action: validate_ref
field: customer_id
collection: customers
error: "Customer not found"
validate_unique
Ensure field values are unique within a scope.
List of fields that must be unique together.
Additional fields defining the uniqueness scope (e.g., tenant_id).
Error message if uniqueness constraint is violated.
Example
pre_create:
- action: validate_unique
fields: [email]
scope: [tenant_id]
error: "A user with this email already exists"
- action: validate_unique
fields: [sku]
scope: [company_id]
error: "A product with this SKU already exists"
Transform a field value using a predefined transformation.
Name of the field to transform.
Transformation type: lowercase, uppercase, trim, trim_left, trim_right.
Example
pre_create:
- action: transform
field: email
transform: lowercase
- action: transform
field: sku
transform: uppercase
http
Make an HTTP request to an external webhook.
HTTP method: GET, POST, PUT, PATCH, DELETE.
Webhook URL. Supports environment variable substitution.
HTTP headers to include in the request.
Request body. Supports context variable expressions.
Execute webhook asynchronously without blocking the operation.
Request timeout in milliseconds.
Number of retry attempts on failure.
Error handling strategy: log (log and continue), fail (abort operation), ignore (silently ignore). Default is log for async hooks, fail for sync hooks.
Only execute webhook if condition evaluates to true.
Example
post_create:
- action: http
method: POST
url: "${ENV.WEBHOOK_URL}/orders/created"
headers:
Authorization: "Bearer ${ENV.WEBHOOK_SECRET}"
Content-Type: "application/json"
body:
event: "order.created"
order_id: $doc._id
order_number: $doc.order_number
company_id: $doc.company_id
total: $doc.total
async: true
timeout: 5000
retry: 3
post_update:
- action: http
condition: $old.status != $new.status
method: POST
url: "${ENV.WEBHOOK_URL}/orders/status-changed"
headers:
Content-Type: "application/json"
body:
event: "order.status_changed"
order_id: $doc._id
old_status: $old.status
new_status: $new.status
async: true
Context Variables
Hooks have access to context variables in expressions:
The current document (for all hooks).
The previous document state (for update/delete hooks).
The new document state with changes (for update hooks).
Current user context from JWT token (contains id, email, role, tenant_id, etc.).
List of changed field names (for post_update hooks).
Auto-generated values like sequences.
Complete Example
version: "1.0"
hooks:
users:
pre_create:
# Auto-set tenant_id from JWT
- action: set_field
field: company_id
value: $user.tenant_id
# Normalize email to lowercase
- action: transform
field: email
transform: lowercase
# Ensure email is unique within company
- action: validate_unique
fields: [email]
scope: [company_id]
error: "A user with this email already exists"
# Validate manager exists
- action: validate_ref
field: manager_id
collection: users
optional: true
error: "Manager not found"
pre_update:
# Normalize email if changed
- action: transform
field: email
transform: lowercase
# Prevent self-management loop
- action: validate
condition: $new.manager_id != $doc._id
error: "User cannot be their own manager"
post_update:
# Sync hierarchy when manager changes
- action: http
condition: $old.manager_id != $new.manager_id
method: POST
url: "http://localhost:8080/_pm/hierarchy/sync"
headers:
Content-Type: "application/json"
body:
user_id: $doc._id
manager_id: $new.manager_id
tenant_id: $doc.company_id
async: false
orders:
pre_create:
# Auto-set ownership fields
- action: set_field
field: company_id
value: $user.tenant_id
- action: set_field
field: created_by
value: $user.id
# Generate order number
- action: set_field
field: order_number
value: $auto.sequence.orders
# Validate customer exists
- action: validate_ref
field: customer_id
collection: customers
error: "Customer not found"
# Validate has items
- action: validate
condition: $doc.items && $doc.items.length > 0
error: "Order must have at least one item"
post_create:
# Notify external system
- action: http
method: POST
url: "${ENV.WEBHOOK_URL}/orders/created"
headers:
Authorization: "Bearer ${ENV.WEBHOOK_SECRET}"
Content-Type: "application/json"
body:
event: "order.created"
order_id: $doc._id
order_number: $doc.order_number
total: $doc.total
async: true
timeout: 5000
retry: 3
pre_update:
# Prevent changing ownership
- action: validate
condition: |
$new.created_by == null || $new.created_by == $old.created_by
error: "Cannot change order owner"
# Status transition validation
- action: validate
condition: |
!($old.status == "delivered" && $new.status != null && $new.status != "delivered")
error: "Cannot change status after delivery"
# Set shipped_at timestamp
- action: set_field
condition: $old.status != "shipped" && $new.status == "shipped"
field: shipped_at
value: $now
post_update:
# Notify on status change
- action: http
condition: $old.status != $new.status
method: POST
url: "${ENV.WEBHOOK_URL}/orders/status-changed"
headers:
Authorization: "Bearer ${ENV.WEBHOOK_SECRET}"
body:
event: "order.status_changed"
order_id: $doc._id
old_status: $old.status
new_status: $new.status
async: true
pre_delete:
# Only allow deleting draft orders
- action: validate
condition: $doc.status == "draft"
error: "Only draft orders can be deleted"
HTTP webhooks receive context information about the operation:
{
"collection": "orders",
"operation": "create",
"document": {
"_id": "507f1f77bcf86cd799439011",
"order_number": "ORD-1001",
"customer_id": "507f1f77bcf86cd799439012",
"total": 99.99
},
"user": {
"id": "507f1f77bcf86cd799439013",
"email": "[email protected]",
"role": "admin",
"tenant_id": "507f1f77bcf86cd799439014"
},
"timestamp": "2026-03-04T12:34:56Z"
}
For update operations, the payload includes both old and new states:
{
"collection": "orders",
"operation": "update",
"old": { "status": "pending" },
"new": { "status": "shipped" },
"changes": ["status", "shipped_at"],
"user": { ... },
"timestamp": "2026-03-04T12:34:56Z"
}
Environment Variables
All string values support environment variable substitution:
post_create:
- action: http
url: "${ENV.WEBHOOK_URL}/notifications"
headers:
Authorization: "Bearer ${ENV.API_KEY}"
Loading Hooks Configuration
Load the hooks configuration at server startup:
permission-mongo --hooks=/path/to/hooks.yml
Or use the PM_HOOKS environment variable:
export PM_HOOKS=/path/to/hooks.yml
permission-mongo
Execution Order
Hooks execute in the order they are defined in the configuration file. If any hook fails:
- Pre-hooks: Operation is aborted, no database changes occur
- Post-hooks (sync): Operation is aborted, changes are rolled back
- Post-hooks (async): Operation completes, error is logged based on
on_error setting