Hook Reference

Reference Complete API

DVP CMS is a truth distillation system for AI-generated content. Plugins are evidence suppliers—they verify facts, pull live data, and make content more trustworthy over time. Learn more →

Complete reference for all DVP CMS plugin hooks. All hooks receive ctx: HookContext as their first parameter (after self) for tenant isolation.

Quick Reference

Hook Type Returns Description
Content Lifecycle
dvp_before_content_createFilterdictModify content before creation
dvp_content_createdActionNoneReact after content created
dvp_before_content_updateFilterdictModify content before update
dvp_content_updatedActionNoneReact after content updated
dvp_before_content_deleteGateboolAllow/deny content deletion
dvp_content_deletedActionNoneReact after content deleted
Publishing Lifecycle
dvp_before_content_publishGatePublishGateResultQuality gate before publish
dvp_content_publishedActionNoneReact after content published
dvp_before_content_unpublishGateboolAllow/deny unpublish
dvp_content_unpublishedActionNoneReact after unpublished
Commands
dvp_before_commandFilterCommandModify command before execution
dvp_after_commandActionNoneReact after command executed
dvp_command_failedFirst-winsAny|NoneProvide fallback on failure
Events
dvp_before_event_publishFilterdictModify event before publishing
dvp_after_event_publishActionNoneReact after event published
SEO Metrics
dvp_seo_metrics_receivedFilterSEOMetricsPayloadEnrich incoming SEO data
dvp_before_pattern_extractionFilterSEOMetricsPayloadFilter before pattern mining
dvp_pattern_discoveredActionNoneReact to pattern discovery
dvp_learning_appliedActionNoneTrack learning application
Scheduled Tasks
dvp_register_scheduled_tasksCollectlistRegister background tasks
dvp_execute_scheduled_taskExecuteTaskResultExecute a scheduled task
dvp_task_completedActionNoneReact after task completes
dvp_task_failedFirst-winsTaskRetryDecision|NoneControl retry behavior
Health Monitoring
dvp_register_health_checksCollectlistRegister health checks
dvp_execute_health_checkExecuteHealthCheckResultExecute health check
dvp_health_status_changedActionNoneReact to status changes
dvp_health_anomaly_detectedActionNoneReact to anomalies

Which Hook For What?

Use this decision matrix to choose the right hook for your use case:

I want to... Use this hook Protocol
Content Modification
Add/modify fields before content is storeddvp_before_content_createContentLifecycleHooks
Transform content on update (add revision, timestamp)dvp_before_content_updateContentLifecycleHooks
Add internal links to contentdvp_before_content_createContentLifecycleHooks
Validation & Gates
Block low-quality content from publishingdvp_before_content_publishContentLifecycleHooks
Prevent deletion of content with dependenciesdvp_before_content_deleteContentLifecycleHooks
Require approval before unpublishingdvp_before_content_unpublishContentLifecycleHooks
Notifications & Side Effects
Send Slack/email when content is publisheddvp_content_publishedContentLifecycleHooks
Update search index after content changesdvp_content_created / dvp_content_updatedContentLifecycleHooks
Invalidate CDN cache on publishdvp_content_publishedContentLifecycleHooks
Scheduled Work
Run daily cleanup/maintenance tasksdvp_register_scheduled_tasksScheduledTaskHooks
Refresh data from external APIs periodicallydvp_register_scheduled_tasksScheduledTaskHooks
Retry failed tasks with custom logicdvp_task_failedScheduledTaskHooks
Monitoring
Monitor domain health/SSL expirydvp_register_health_checksHealthMonitorHooks
Detect anomalies in metricsdvp_health_anomaly_detectedHealthMonitorHooks
Integrate external SEO data (GSC, Ahrefs)dvp_seo_metrics_receivedSEOMetricsHooks
Note: Looking for AI generation hooks? ContentGeneratorHooks is planned but not yet implemented. For now, use dvp_before_content_create or dvp_before_content_update to process content after generation.

Hook Types

Filter Hooks

Modify and return a value. The return value replaces the input.

def dvp_before_content_create(
    self,
    ctx: HookContext,
    content: dict[str, Any],
    metadata: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Filter hook - MUST return the modified content."""
    content['processed'] = True
    return content  # Required!

Key Rules:

Action Hooks

Perform side effects without returning a value.

def dvp_content_created(
    self,
    ctx: HookContext,
    content_id: str,
    content: dict[str, Any],
    metadata: dict[str, Any] | None = None,
) -> None:
    """Action hook - no return needed."""
    self.logger.info(f"Content created: {content_id}")
    # No return

Key Rules:

Gate Hooks

Allow or deny an operation.

def dvp_before_content_delete(
    self,
    ctx: HookContext,
    content_id: str,
) -> bool:
    """Gate hook - return False to block."""
    if self.has_dependencies(content_id):
        return False  # Block deletion
    return True  # Allow deletion

Key Rules:

First-Wins Hooks

First non-None return value is used.

def dvp_command_failed(
    self,
    ctx: HookContext,
    command: Any,
    error: Exception,
) -> Any | None:
    """First-wins hook - first non-None wins."""
    if self.can_recover(error):
        return self.recover(command)
    return None  # Let next plugin try

Key Rules:

Collect Hooks

Aggregate results from all plugins.

def dvp_register_scheduled_tasks(
    self,
    ctx: HookContext,
) -> list[ScheduledTaskDefinition]:
    """Collect hook - all results aggregated."""
    return [
        ScheduledTaskDefinition(
            task_id="my-plugin.cleanup",
            name="Daily Cleanup",
            schedule=TaskSchedule(cron_expression="0 3 * * *"),
        ),
    ]

Key Rules:

Return Types

These types are used as return values for specific hooks:

PublishGateResult

Returned by dvp_before_content_publish to allow or block publishing:

@dataclass
class PublishGateResult:
    allow: bool                    # True to allow, False to block
    reason: str | None = None      # Human-readable explanation
    blocked_by: str | None = None  # Plugin name that blocked
    details: dict[str, Any] = field(default_factory=dict)  # Additional context

Usage:

def dvp_before_content_publish(self, ctx, content, metadata=None):
    score = content.get('quality_score', 0)
    if score < 70:
        return PublishGateResult(
            allow=False,
            reason=f"Quality score {score} below threshold (70)",
            blocked_by=self.name,
            details={"score": score, "threshold": 70}
        )
    return PublishGateResult(allow=True)

TaskResult

Returned by dvp_execute_scheduled_task:

@dataclass
class TaskResult:
    success: bool                      # Whether task succeeded
    data: dict[str, Any] | None = None # Task output data
    error_message: str | None = None   # Error details if failed
    duration_ms: int | None = None     # Execution time

HealthCheckResult

Returned by dvp_execute_health_check:

@dataclass
class HealthCheckResult:
    status: HealthStatus         # HEALTHY, DEGRADED, UNHEALTHY, UNKNOWN
    message: str | None = None   # Status explanation
    details: dict[str, Any] = field(default_factory=dict)

HookContext

All hooks receive a HookContext object providing tenant and tracing information:

@dataclass
class HookContext:
    tenant_id: UUID           # Tenant identifier
    tenant_slug: str          # Human-readable tenant name
    tenant_plan: str          # Tenant's subscription plan
    correlation_id: UUID | None = None   # Request correlation ID
    causation_id: UUID | None = None     # Event causation ID
    timestamp: datetime | None = None    # Event timestamp
    metadata: dict[str, Any] = field(default_factory=dict)

Usage:

def dvp_content_created(self, ctx, content_id, content, metadata=None):
    # Tenant isolation
    tenant_config = self.get_config(str(ctx.tenant_id))

    # Feature gating by plan
    if ctx.tenant_plan == "enterprise":
        self.enable_advanced_features()

    # Logging with correlation
    self.logger.info(
        f"Content created for {ctx.tenant_slug}",
        extra={"correlation_id": str(ctx.correlation_id)},
    )

Content Lifecycle Hooks

dvp_before_content_create

Called before content is created. Modify content before storage.

def dvp_before_content_create(
    self,
    ctx: HookContext,
    content: dict[str, Any],
    metadata: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """
    Type: Filter hook
    Returns: Modified content dict (never None!)
    """
    content['created_by_plugin'] = self.name
    content['tenant_id'] = str(ctx.tenant_id)
    return content

Priority Recommendations:

dvp_content_created

Called after content is created. Use for notifications, indexing, etc.

def dvp_content_created(
    self,
    ctx: HookContext,
    content_id: str,
    content: dict[str, Any],
    metadata: dict[str, Any] | None = None,
) -> None:
    """
    Type: Action hook
    Returns: None
    """
    self.send_notification(f"New content: {content_id}")
    self.update_search_index(ctx.tenant_id, content_id, content)

dvp_before_content_update

Called before content is updated. Modify new content before storage.

def dvp_before_content_update(
    self,
    ctx: HookContext,
    new_content: dict[str, Any],
    content_id: str,
    old_content: dict[str, Any],
) -> dict[str, Any]:
    """
    Type: Filter hook
    Returns: Modified new_content dict
    """
    new_content['updated_at'] = datetime.now().isoformat()
    new_content['revision'] = old_content.get('revision', 0) + 1
    return new_content

dvp_before_content_delete

Called before content deletion. Return False to prevent.

def dvp_before_content_delete(
    self,
    ctx: HookContext,
    content_id: str,
) -> bool:
    """
    Type: Gate hook
    Returns: True to allow, False to block
    """
    if self.has_dependencies(ctx.tenant_id, content_id):
        self.logger.warning(f"Cannot delete {content_id}: has dependencies")
        return False
    return True

Publishing Lifecycle Hooks

dvp_before_content_publish

Quality gate before content is published.

def dvp_before_content_publish(
    self,
    ctx: HookContext,
    content: dict[str, Any],
    metadata: dict[str, Any] | None = None,
) -> PublishGateResult:
    """
    Type: Gate hook
    Returns: PublishGateResult with allow=True/False
    """
    if content.get('quality_score', 0) < 70:
        return PublishGateResult(
            allow=False,
            reason="Quality score below threshold",
            blocked_by=self.name,
        )
    return PublishGateResult(allow=True)

dvp_content_published

Called after content is published. Trigger deployments, invalidate caches.

async def dvp_content_published(
    self,
    ctx: HookContext,
    content_id: str,
    content: dict[str, Any],
    metadata: dict[str, Any] | None = None,
) -> None:
    """
    Type: Action hook
    Returns: None
    """
    # Trigger deployment
    await self.vercel_client.create_deployment(project_id=self.config.project_id)

    # Invalidate CDN
    domain = content.get('domain')
    self.cdn_client.invalidate(f"/{domain}/*")

Hook Priority

Plugins can specify execution order via get_hook_priority():

class MyPlugin(Plugin, ContentLifecycleHooks):
    def get_hook_priority(self, hook_name: str) -> int:
        """
        Lower number = runs first
        Default: 100
        """
        if hook_name == 'dvp_before_content_create':
            return 10  # Run early (validation)
        return 100  # Default priority

Recommended Priority Ranges:

Implementing Hooks

Basic Pattern

from typing import TYPE_CHECKING, Any
from dvp_cms.plugins.base import Plugin
from dvp_cms.plugins.hookspec import ContentLifecycleHooks

if TYPE_CHECKING:
    from dvp_cms.plugins.context import HookContext


class MyPlugin(Plugin, ContentLifecycleHooks):
    name = "my-plugin"
    version = "1.0.0"

    def dvp_before_content_create(
        self,
        ctx: "HookContext",
        content: dict[str, Any],
        metadata: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        content['processed'] = True
        return content

Async Hooks

Hooks can be sync or async—the system handles both:

async def dvp_content_published(
    self,
    ctx: "HookContext",
    content_id: str,
    content: dict[str, Any],
    metadata: dict[str, Any] | None = None,
) -> None:
    await self.external_api.notify(content_id)

Testing Hooks

from dvp_cms.plugins.testing import make_test_hook_ctx

def test_hook_modifies_content():
    plugin = MyPlugin()
    ctx = make_test_hook_ctx()

    content = {'title': 'Test'}
    result = plugin.dvp_before_content_create(ctx, content)

    assert result is not None
    assert result['processed'] is True

See Also