Creating Record Triggers in Valstorm

Record triggers in Valstorm allow you to inject custom automations into the standard Create, Update, and Delete API request lifecycle. This enables you to run specific logic at different stages of a data operation.


Trigger Contexts

You can configure your triggers to execute in one of three distinct contexts:

  • Before: Runs synchronously before the database operation. Ideal for data validation, permission checks, or modifying data in-place before it is saved.
  • After: Runs synchronously after the database commit but before the API response is sent. Use this for immediate side effects.
  • Async: Runs asynchronously in a background task after the request is complete. Perfect for heavy lifting like sending emails or syncing to external APIs.

The V2 Trigger Structure

All V2 triggers use a standardized function signature that accepts a context object. This object provides efficient access to the data being processed.

The execute Function

Every trigger must define an execute function with the following signature:

from valstorm.trigger_context import RecordTriggerContext def execute(context: RecordTriggerContext): # Your logic here pass

The RecordTriggerContext Object

The context object contains all the data you need:

PropertyTypeDescription
context.userUserThe user performing the API request.
context.new_mapdictA dictionary of {id: record} representing the new state.
context.old_mapdictA dictionary of {id: record} representing the previous state.
context.trigger_contextsetA set of strings indicating the operation (e.g., {'Before', 'Create'}).

Code Patterns

1. Before Create / Update

Use this to validate data or modify it before saving. Modifications to context.new_map are saved automatically.

from fastapi import HTTPException from valstorm.dependencies import add_log from valstorm.trigger_context import RecordTriggerContext def execute(context: RecordTriggerContext): try: # Example: Enforce a naming convention for record in context.new_map.values(): if not record.get('name'): record['name'] = 'Auto-Generated Name' # Example: Block specific changes old_record = context.old_map.get(record['id']) if old_record and old_record.get('status') == 'Locked': raise HTTPException(status_code=400, detail="Cannot edit locked records.") except HTTPException as e: raise e # Blocking error except Exception as e: add_log(f"Error in Before Trigger: {e}", 'error')

2. After Create / Update

Use this for immediate actions that require the record to be saved first.

from valstorm.models import SendNotificationSetting from valstorm.dependencies import add_log from valstorm.notifications import send_notifications_handler from valstorm.trigger_context import RecordTriggerContext def execute(context: RecordTriggerContext): try: notifications = [] for record in context.new_map.values(): notifications.append(SendNotificationSetting( name="New Record Alert", data={ 'title': 'Record Created', 'body': f"{record.get('name')} was created.", 'id': record['id'] } )) if notifications: send_notifications_handler(notifications, context.user) except Exception as e: add_log(f"Error in After Trigger: {e}", 'error')

3. Delete (Before / After / Async)

For delete triggers, iterate over context.old_map because new_map is empty.

from valstorm.dependencies import add_log from valstorm.trigger_context import RecordTriggerContext def execute(context: RecordTriggerContext): try: # Iterate old_map to see what is being deleted for record in context.old_map.values(): add_log(f"Record {record['id']} is being deleted.", 'info') except Exception as e: add_log(f"Error in Delete Trigger: {e}", 'error')