Skip to content

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:

cd cool-stuff
lime-project generate limeobject

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:

  1. 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
  2. Automations - Triggered for all objects in the unit of work

    • Objects added here won't trigger automations but may trigger before_update
  3. 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_update instead
  4. 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:
class Person(LimeObject):
  • Change what limetype to override in the register_limeobject_classes function in the file:
def register_limeobject_classes(register_class):
    register_class('person', Person)

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.

copy_of_person = person.copy()

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_update instead)
  • ❌ Computed properties (use prepare_update instead)

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_update for validations, computed properties, API calls, file I/O, and any other logic
  • ⚠️ Only use before_update in very special cases where logic must run after automations
  • ✅ Always add modified objects to the provided uow parameter
  • ✅ Never call uow.commit() manually - it's handled automatically
  • ❌ Never create a new UnitOfWork instance 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.