# How to Use Pre-Save and Post-Delete Hooks on a StructuredNode in Neomodel

> Learn how to use pre-save and post-delete hooks on Neomodel StructuredNode. Discover how the @hooks decorator simplifies persisting data in Neo4j.

- Repository: [Neo4j Contrib/neomodel](https://github.com/neo4j-contrib/neomodel)
- Tags: how-to-guide
- Published: 2026-03-08

---

**Use `pre_save`, `post_save`, `pre_delete`, and `post_delete` methods on your `StructuredNode` subclass—the `@hooks` decorator in `neomodel` automatically invokes them before and after persistence operations.**

The `neo4j-contrib/neomodel` library provides a built-in lifecycle hook system that lets you execute custom logic when nodes are saved to or removed from Neo4j. By implementing specific method signatures on a `StructuredNode`, you can run validation, auditing, or cascading operations without overriding core persistence logic.

## Understanding the Hook Lifecycle in Neomodel

`neomodel` supports six distinct lifecycle hooks that map to the core persistence operations in `StructuredNode`. The hook mechanism is implemented via the `@hooks` decorator located in [`neomodel/hooks.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/hooks.py), which wraps the `save()` and `delete()` methods defined in [`neomodel/sync_/node.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/sync_/node.py).

When you define a method matching one of the supported patterns, the decorator automatically invokes it at the appropriate time:

| Operation | Hook Method | Execution Timing |
|-----------|-------------|------------------|
| `save()` | `pre_save` | Immediately before the node is persisted or updated |
| `save()` | `post_save` | Immediately after the node is written to the database |
| `delete()` | `pre_delete` | Immediately before the node is removed from the graph |
| `delete()` | `post_delete` | Immediately after the node is detached and marked deleted |
| `create()` | `post_create` | After a new node is created via `create()` (internally used by `save`) |
| `create()` | `pre_create` | Rarely used; only if calling `create()` directly |

## How the Hook Mechanism Works Internally

### The @hooks Decorator Implementation

The `@hooks` decorator in [`neomodel/hooks.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/hooks.py) inspects the wrapped function name and constructs the corresponding pre- and post-hook method names. It then calls `_exec_hook` to check if the instance implements these methods and executes them if present:

```python

# neomodel/hooks.py

def _exec_hook(hook_name: str, self: Any) -> None:
    if hasattr(self, hook_name):
        getattr(self, hook_name)()

```

### Integration with StructuredNode Methods

Both `save()` and `delete()` in [`neomodel/sync_/node.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/sync_/node.py) are decorated with `@hooks`:

```python

# neomodel/sync_/node.py

@hooks
def save(self) -> "StructuredNode":
    # Core persistence logic

    ...

@hooks
def delete(self) -> bool:
    # Core deletion logic

    ...

```

When you call `instance.save()`, the decorator executes `pre_save` → core save logic → `post_save`. Similarly, `instance.delete()` triggers `pre_delete` → core delete logic → `post_delete`.

## Implementing Pre-Save and Post-Delete Hooks

### Basic Hook Implementation

Define methods with the exact names `pre_save`, `post_save`, `pre_delete`, or `post_delete` on your `StructuredNode` subclass:

```python
from neomodel import StructuredNode, StringProperty

class Person(StructuredNode):
    name = StringProperty()

    def pre_save(self):
        print(f"[pre_save] About to save {self}")

    def post_save(self):
        print(f"[post_save] Saved node with id {self.element_id}")

    def pre_delete(self):
        print(f"[pre_delete] Deleting node {self}")

    def post_delete(self):
        print("[post_delete] Node removed")

```

```python

# Usage

bob = Person(name="Bob").save()  # → triggers pre_save → post_save

bob.delete()                     # → triggers pre_delete → post_delete

```

### Validation with Pre-Save Hooks

Use `pre_save` to enforce business rules before persistence occurs. Raising an exception prevents the save operation:

```python
class Product(StructuredNode):
    sku = StringProperty(unique_index=True)
    name = StringProperty()

    def pre_save(self):
        if not self.sku:
            raise ValueError("SKU must be set before saving")

```

If `sku` is missing, `save()` raises `ValueError` before executing any Cypher queries.

### Auditing with Post-Delete Hooks

Implement `post_delete` to create audit trails or trigger cleanup operations after removal:

```python
class AuditLog(StructuredNode):
    action = StringProperty()
    node_id = StringProperty()

class Order(StructuredNode):
    number = StringProperty()

    def post_delete(self):
        AuditLog(action="order_deleted", node_id=self.element_id).save()

```

When an `Order` is deleted, `post_delete` automatically creates an `AuditLog` entry.

## Testing Hook Behavior

The official test suite in [`test/sync_/test_hooks.py`](https://github.com/neo4j-contrib/neomodel/blob/main/test/sync_/test_hooks.py) demonstrates the complete hook execution flow:

```python

# test/sync_/test_hooks.py

class HookTest(StructuredNode):
    name = StringProperty()

    def post_create(self):
        HOOKS_CALLED["post_create"] = 1

    def pre_save(self):
        HOOKS_CALLED["pre_save"] = 1

    def post_save(self):
        HOOKS_CALLED["post_save"] = 1

    def pre_delete(self):
        HOOKS_CALLED["pre_delete"] = 1

    def post_delete(self):
        HOOKS_CALLED["post_delete"] = 1

```

```python
ht = HookTest(name="k").save()   # → pre_save, post_save, post_create

ht.delete()                       # → pre_delete, post_delete

assert "pre_save"    in HOOKS_CALLED
assert "post_save"   in HOOKS_CALLED
assert "post_create" in HOOKS_CALLED
assert "pre_delete"  in HOOKS_CALLED
assert "post_delete" in HOOKS_CALLED

```

The async implementation follows the same pattern; see [`test/async_/test_hooks.py`](https://github.com/neo4j-contrib/neomodel/blob/main/test/async_/test_hooks.py) for `AsyncStructuredNode` examples.

## Summary

- **Hook mechanism**: The `@hooks` decorator in [`neomodel/hooks.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/hooks.py) automatically triggers `pre_save`, `post_save`, `pre_delete`, and `post_delete` methods on your `StructuredNode` subclass.
- **Implementation**: Define methods with exact names matching the hook pattern; no registration or configuration required.
- **Execution order**: `pre_save` runs before persistence, `post_save` after; `pre_delete` runs before removal, `post_delete` after detachment.
- **Use cases**: Validation (raise exceptions in `pre_save`), auditing (create logs in `post_delete`), cascading updates, and side-effect management.
- **Source locations**: Core logic resides in [`neomodel/hooks.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/hooks.py) and [`neomodel/sync_/node.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/sync_/node.py), with comprehensive tests in [`test/sync_/test_hooks.py`](https://github.com/neo4j-contrib/neomodel/blob/main/test/sync_/test_hooks.py).

## Frequently Asked Questions

### What is the difference between pre_save and post_create hooks?

The `pre_save` hook runs before every save operation, including both creating new nodes and updating existing ones. The `post_create` hook only fires after a new node is created via the `create()` method, which is called internally during the initial save. Use `pre_save` for validation that applies to both inserts and updates, and `post_create` for initialization logic specific to new instances.

### Can I raise exceptions in pre_save hooks to prevent saving?

Yes. Because `pre_save` executes before any Cypher queries are sent to Neo4j, raising an exception inside the method aborts the save operation entirely. The database remains unchanged, and the exception propagates to your application code. This pattern is ideal for enforcing business rules or validating required fields before persistence.

### Do hooks work with async StructuredNode in neomodel?

Yes. The hook mechanism works identically for `AsyncStructuredNode` in the async API. The `@hooks` decorator is applied to the async versions of `save()` and `delete()` in [`neomodel/async_/node.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/async_/node.py). You define `pre_save`, `post_save`, `pre_delete`, and `post_delete` methods exactly as you would in the sync version, and they will be awaited appropriately during the async lifecycle.

### How do I implement cascading deletes using post_delete hooks?

Implement the `post_delete` method on your parent node to manually remove related nodes or perform cleanup operations. Because `post_delete` runs after the node has been detached from the graph and marked as deleted, you can safely access related nodes or properties to determine what additional cleanup is required. For example, you can query for related children and delete them, or create audit records referencing the deleted node's ID before it becomes unavailable.