Custom lime objects¶
Custom lime objects allows for custom logic to be executed when limeobjects are added, updated or deleted in CRM.
Understanding transaction hooks
Custom limeobjects provide several hooks that run at different stages during the save process. It's crucial to understand when each hook runs and what operations are appropriate in each context. Misusing these hooks, especially performing heavy work or nested transactions during before_update, can lead to serious performance issues, deadlocks, and system instability.
Running the following:
Information
If you already have a limeobject_classes folder in your solution or package. Copy one of the existing LimeObject files instead of running above command.
Information
It's a good practice to name the limeobject_class.py file to what limetype it handles. i.e. person.py or limeobject_person.py
Understanding the hook lifecycle¶
When uow.commit() is called, the following hooks are executed in this specific order:
-
prepare_update(uow, **kwargs)- Runs BEFORE the database transaction starts ⭐ PREFERRED- This is the recommended hook for most use cases
- Best place for expensive operations (API calls, file operations, complex calculations)
- Best place for validations and computed properties
- Reduces risk of deadlocks and transaction timeouts
- Changes made here are persisted
- Objects added here can trigger automations
- Available since Lime CRM 2023.1
-
Automations - Triggered for all objects in the unit of work
- Objects added here won't trigger automations but may trigger
before_update
- Objects added here won't trigger automations but may trigger
-
before_update(uow, **kwargs)- Runs WITHIN the database transaction ⚠️ USE SPARINGLY- Only use in very special cases where logic must run after automations
- Changes made here are persisted
- Objects added here won't trigger automations
- ⚠️ Keep operations extremely lightweight - heavy work causes deadlocks and performance issues
- ⚠️ Avoid using this hook if possible - prefer
prepare_updateinstead
-
SQL UPDATE - Database statements are executed
If you want to create a custom limeobject for another limetype do the following:
- Change the name of the class:
- Change what limetype to override in the
register_limeobject_classesfunction in the file:
register_class('person', Person) yields the following code in
cool-stuff/cool_stuff/limeobject_classes/limeobject_class.py:
class Person(LimeObject):
"""Summarize the function of a person object here"""
def prepare_update(self, uow, **kwargs):
"""
This is called BEFORE the database transaction starts.
Use this hook for expensive operations like API calls,
file operations, or complex calculations.
All changes made to the object here will be persisted.
All other objects that are changed or created here must be
added to the unit of work.
"""
# Example: Perform expensive validation or data enrichment
# before the transaction starts
pass
def before_update(self, uow, **kwargs):
"""
This is called WITHIN the database transaction on all new
and updated objects. Keep operations lightweight here.
All changes made to the object here will be persisted.
All other objects that are changed or created here must be
added to the unit of work.
"""
# Lightweight operation - simple string concatenation is fine here
self.properties.name.value = '{} {}'.format(
self.properties.firstname.value,
self.properties.lastname.value)
def before_delete(self, uow, **kwargs):
"""
This is called on objects that are about to be deleted,
before relations are detached. All changes made to
the object here will be persisted.
All other objects that are changed or created here must be
added to the unit of work.
"""
pass
def register_limeobject_classes(register_class):
register_class('person', Person)
it also creates the following files:
cool-stuff/cool_stuff/limeobject_classes/limeobject_class_test.py(Example of tests)cool-stuff/cool_stuff/limeobject_classes/__init__.py(For registering the custom lime objects)
This generates a custom limeobject and registers it to handle objects of
type person in Lime CRM. This means that whenever you create a
limeobject of type person, or whenever you retrieve a person from the
database, this class will be instantiated instead of the default
LimeObject
The code generated for our package adds hooks for before a person is about to be saved to the database.
In before_update we set the property name to be the concatenation of
the person's first and last name.
Information
If you want to execute something only when a limeobject has been created as a copy of another limeobject you can check for the is_copy property of the limeobject itself.
class Person(LimeObject):
def before_update(self, uow, **kwargs):
if self.is_copy:
# Perform custom logic if this Person is a copy of another Person
...
A limeobject is only marked as a copy of another limeobject when the copy has been created with the copy() function on a limeobject instance.
Best practices and common pitfalls¶
Prefer prepare_update over before_update
In almost all cases, you should use prepare_update instead of before_update. The prepare_update hook runs before the database transaction starts, making it safer and more performant. Only use before_update in very special cases where your logic absolutely must run after automations have executed.
⚠️ Avoid before_update - use prepare_update instead¶
The before_update hook runs while a database transaction is open. This means the database connection is locked and waiting for your code to complete. Performing operations in this hook is problematic because:
Why working with an open database connection is bad:
- Deadlocks: Long-running transactions increase the risk of database deadlocks, where multiple transactions wait for each other to release locks, causing the system to become unresponsive
- Transaction timeouts: If your code takes too long, the database transaction may timeout, causing the save operation to fail
- Blocking other users: While your transaction is open, other users may be blocked from accessing the same data, degrading system performance
- Resource exhaustion: Open database connections consume server resources (memory, connection pool slots), potentially affecting the entire system
- Cascading failures: If external services are slow or unavailable, the database transaction remains open, blocking additional threads that encounter the same service, creating a cascading resource exhaustion scenario
Operations to avoid in before_update:
- ❌ HTTP requests to external APIs (even with timeouts)
- ❌ File operations (reading, writing, uploading files)
- ❌ Complex calculations or data processing
- ❌ Sending emails
- ❌ Querying external databases
- ❌ Any operation that might take more than a few milliseconds
- ❌ Validations (use
prepare_updateinstead) - ❌ Computed properties (use
prepare_updateinstead)
When to use before_update
The before_update hook should only be used in very special cases where your logic must run after automations have been triggered. For example, if an automation modifies a field and your custom logic needs to react to that change. In 99% of cases, you should use prepare_update instead.
✅ Use prepare_update as your default choice¶
The prepare_update hook runs before the database transaction starts, making it the perfect place for almost all custom logic:
# Add this import at the top of your file
import requests
from lime_application import LimeApplication
from lime_types import LimeObject
class Person(LimeObject):
"""Example showing proper use of prepare_update"""
def prepare_update(self, uow, **kwargs):
"""
This runs BEFORE the database transaction starts.
This is the RECOMMENDED hook for most use cases.
Use this for API calls, validations, computed properties, etc.
"""
# Validations - perfectly safe here
if not self.properties.firstname.value:
raise ValueError("First name is required")
# Computed properties - perfectly safe here
self.properties.name.value = '{} {}'.format(
self.properties.firstname.value,
self.properties.lastname.value
)
if self.is_new:
# External API calls - perfectly safe here
response = requests.get(
'https://api.example.com/verify',
params={'email': self.properties.email.value},
timeout=30
)
if response.status_code == 200:
# Store the result in a property
self.properties.verified.value = True
def before_update(self, uow, **kwargs):
"""
This runs WITHIN the database transaction.
Only use this in very special cases where logic must run after automations.
Keep operations extremely lightweight if you must use this hook.
"""
# Only use this hook if you absolutely need to run after automations
pass
🚫 NEVER create nested transactions (new Unit of Work)¶
Creating a new Unit of Work inside a hook that already runs within a transaction is extremely dangerous and will cause serious problems:
class Person(LimeObject):
"""Example of what NOT to do - nested transactions"""
def before_update(self, uow, **kwargs):
"""BAD EXAMPLE - DO NOT DO THIS!"""
# ❌ NEVER create a new Unit of Work inside a hook
# This creates a nested transaction which is FORBIDDEN
# (The exact way to create a UnitOfWork doesn't matter -
# creating one inside any hook is always wrong!)
# This will cause deadlocks and data corruption!
new_uow = self.application.database.create_unit_of_work() # ❌ NEVER DO THIS
# Loading and modifying objects with a new UoW
company = self.application.limeobjects.get_limeobject('company', self.properties.idcompany.value)
company.properties.note.value = 'Updated by nested transaction'
new_uow.add(company)
new_uow.commit() # ❌ NESTED TRANSACTION - DISASTER!
Why nested transactions are catastrophic:
- Database deadlocks: The outer transaction holds locks while the inner transaction tries to acquire overlapping locks, causing immediate deadlocks
- Data inconsistency: If the inner transaction commits but the outer transaction rolls back, you have corrupted data
- Unpredictable behavior: Database systems aren't designed to handle nested transactions from application code, leading to undefined behavior
- Lock escalation: Multiple transactions from the same process can cause lock escalation, blocking the entire system
- Difficult debugging: Nested transaction issues are extremely difficult to diagnose and debug
The correct approach:
Instead of creating nested transactions, add objects to the existing Unit of Work:
class Person(LimeObject):
"""Correct way to modify related objects"""
def before_update(self, uow, **kwargs):
"""
Correct approach - use the provided Unit of Work
"""
# ✅ Modify related objects and add them to the EXISTING UoW
company = self.application.limeobjects.get_limeobject(
'company',
self.properties.idcompany.value
)
company.properties.note.value = 'Updated properly'
# Add to the existing Unit of Work passed as a parameter
uow.add(company)
# ✅ Never call uow.commit() yourself
# The system will commit everything together automatically
General guidelines¶
- ⭐ Always prefer
prepare_update- it's the recommended hook for almost all use cases - ✅ Use
prepare_updatefor validations, computed properties, API calls, file I/O, and any other logic - ⚠️ Only use
before_updatein very special cases where logic must run after automations - ✅ Always add modified objects to the provided
uowparameter - ✅ Never call
uow.commit()manually - it's handled automatically - ❌ Never create a new
UnitOfWorkinstance inside any hook - ❌ Never perform HTTP requests or file operations in
before_update - ❌ Never attempt nested transactions by any means
Following these guidelines ensures your custom limeobjects perform well, don't cause deadlocks, and maintain data integrity.