From 50e3fce12b4f682ee4864224c0e9748e3c4ec7cf Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Sat, 24 Jan 2026 02:06:49 +0100
Subject: [PATCH] fixed automation and trustee
---
app.py | 3 +
.../automation/routeFeatureAutomation.py | 30 +--
modules/features/featureRegistry.py | 10 +
modules/features/trustee/mainTrustee.py | 216 +++++++++++++++++-
modules/interfaces/interfaceDbChat.py | 25 +-
modules/routes/routeAdminFeatures.py | 15 +-
modules/shared/attributeUtils.py | 4 +-
modules/workflows/automation/mainWorkflow.py | 28 ++-
8 files changed, 296 insertions(+), 35 deletions(-)
diff --git a/app.py b/app.py
index 4e3e0c67..57274338 100644
--- a/app.py
+++ b/app.py
@@ -47,6 +47,9 @@ class DailyRotatingFileHandler(RotatingFileHandler):
def _updateFileIfNeeded(self):
"""Update the log file if the date has changed"""
+ # Guard against interpreter shutdown when datetime may be None
+ if datetime is None:
+ return False
today = datetime.now().strftime("%Y%m%d")
if self.currentDate != today:
diff --git a/modules/features/automation/routeFeatureAutomation.py b/modules/features/automation/routeFeatureAutomation.py
index 49c1606e..4eef9381 100644
--- a/modules/features/automation/routeFeatureAutomation.py
+++ b/modules/features/automation/routeFeatureAutomation.py
@@ -14,7 +14,7 @@ import json
# Import interfaces and models
from modules.interfaces.interfaceDbChat import getInterface as getChatInterface
-from modules.auth import getCurrentUser, limiter
+from modules.auth import getCurrentUser, limiter, getRequestContext, RequestContext
from modules.features.automation.datamodelFeatureAutomation import AutomationDefinition
from modules.datamodels.datamodelChat import ChatWorkflow
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
@@ -46,7 +46,7 @@ router = APIRouter(
async def get_automations(
request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
- currentUser = Depends(getCurrentUser)
+ context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[AutomationDefinition]:
"""
Get automation definitions with optional pagination, sorting, and filtering.
@@ -69,7 +69,7 @@ async def get_automations(
detail=f"Invalid pagination parameter: {str(e)}"
)
- chatInterface = getChatInterface(currentUser)
+ chatInterface = getChatInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None, featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None)
result = chatInterface.getAllAutomationDefinitions(pagination=paginationParams)
# If pagination was requested, result is PaginatedResult
@@ -111,11 +111,11 @@ async def get_automations(
async def create_automation(
request: Request,
automation: AutomationDefinition,
- currentUser = Depends(getCurrentUser)
+ context: RequestContext = Depends(getRequestContext)
) -> AutomationDefinition:
"""Create a new automation definition"""
try:
- chatInterface = getChatInterface(currentUser)
+ chatInterface = getChatInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None, featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None)
automationData = automation.model_dump()
created = chatInterface.createAutomationDefinition(automationData)
return created
@@ -132,7 +132,7 @@ async def create_automation(
@limiter.limit("30/minute")
async def get_automation_templates(
request: Request,
- currentUser = Depends(getCurrentUser)
+ context: RequestContext = Depends(getRequestContext)
) -> JSONResponse:
"""
Get automation templates from backend module.
@@ -160,11 +160,11 @@ async def get_automation_attributes(
async def get_automation(
request: Request,
automationId: str = Path(..., description="Automation ID"),
- currentUser = Depends(getCurrentUser)
+ context: RequestContext = Depends(getRequestContext)
) -> AutomationDefinition:
"""Get a single automation definition by ID"""
try:
- chatInterface = getChatInterface(currentUser)
+ chatInterface = getChatInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None, featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None)
automation = chatInterface.getAutomationDefinition(automationId)
if not automation:
raise HTTPException(
@@ -188,11 +188,11 @@ async def update_automation(
request: Request,
automationId: str = Path(..., description="Automation ID"),
automation: AutomationDefinition = Body(...),
- currentUser = Depends(getCurrentUser)
+ context: RequestContext = Depends(getRequestContext)
) -> AutomationDefinition:
"""Update an automation definition"""
try:
- chatInterface = getChatInterface(currentUser)
+ chatInterface = getChatInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None, featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None)
automationData = automation.model_dump()
updated = chatInterface.updateAutomationDefinition(automationId, automationData)
return updated
@@ -215,11 +215,11 @@ async def update_automation(
async def delete_automation(
request: Request,
automationId: str = Path(..., description="Automation ID"),
- currentUser = Depends(getCurrentUser)
+ context: RequestContext = Depends(getRequestContext)
) -> Response:
"""Delete an automation definition"""
try:
- chatInterface = getChatInterface(currentUser)
+ chatInterface = getChatInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None, featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None)
success = chatInterface.deleteAutomationDefinition(automationId)
if success:
return Response(status_code=204)
@@ -244,15 +244,15 @@ async def delete_automation(
@router.post("/{automationId}/execute", response_model=ChatWorkflow)
@limiter.limit("5/minute")
-async def execute_automation(
+async def execute_automation_route(
request: Request,
automationId: str = Path(..., description="Automation ID"),
- currentUser = Depends(getCurrentUser)
+ context: RequestContext = Depends(getRequestContext)
) -> ChatWorkflow:
"""Execute an automation immediately (test mode)"""
try:
from modules.services import getInterface as getServices
- services = getServices(currentUser, None)
+ services = getServices(context.user, context.mandateId)
workflow = await executeAutomation(automationId, services)
return workflow
except HTTPException:
diff --git a/modules/features/featureRegistry.py b/modules/features/featureRegistry.py
index 29eb35c9..4bf6d82e 100644
--- a/modules/features/featureRegistry.py
+++ b/modules/features/featureRegistry.py
@@ -37,6 +37,7 @@ def discoverFeatureContainers() -> List[str]:
def loadFeatureRouters(app: FastAPI) -> Dict[str, Any]:
"""
Dynamically load and register routers from all discovered feature containers.
+ Also registers feature template roles and AccessRules in the database.
"""
results = {}
pattern = os.path.join(FEATURES_DIR, "*", "routeFeature*.py")
@@ -64,6 +65,15 @@ def loadFeatureRouters(app: FastAPI) -> Dict[str, Any]:
logger.error(f"Failed to load router from {featureDir}: {e}")
results[featureDir] = {"status": "error", "error": str(e)}
+ # Register features in RBAC catalog and sync template roles to database
+ from modules.security.rbacCatalog import getCatalogService
+ catalogService = getCatalogService()
+ registrationResults = registerAllFeaturesInCatalog(catalogService)
+
+ for featureName, success in registrationResults.items():
+ if featureName in results:
+ results[featureName]["rbac_registered"] = success
+
return results
diff --git a/modules/features/trustee/mainTrustee.py b/modules/features/trustee/mainTrustee.py
index 176317a7..8cda0cd0 100644
--- a/modules/features/trustee/mainTrustee.py
+++ b/modules/features/trustee/mainTrustee.py
@@ -103,7 +103,8 @@ RESOURCE_OBJECTS = [
},
]
-# Template roles for this feature
+# Template roles for this feature with AccessRules
+# Each role defines default UI and DATA permissions
TEMPLATE_ROLES = [
{
"roleLabel": "trustee-admin",
@@ -111,7 +112,13 @@ TEMPLATE_ROLES = [
"en": "Trustee Administrator - Full access to all trustee data and settings",
"de": "Treuhand-Administrator - Vollzugriff auf alle Treuhand-Daten und Einstellungen",
"fr": "Administrateur fiduciaire - Accès complet aux données et paramètres fiduciaires"
- }
+ },
+ "accessRules": [
+ # Full UI access
+ {"context": "UI", "item": None, "view": True},
+ # Full DATA access
+ {"context": "DATA", "item": None, "view": True, "read": "a", "create": "a", "update": "a", "delete": "a"},
+ ]
},
{
"roleLabel": "trustee-accountant",
@@ -119,7 +126,13 @@ TEMPLATE_ROLES = [
"en": "Trustee Accountant - Manage accounting and financial data",
"de": "Treuhand-Buchhalter - Buchhaltungs- und Finanzdaten verwalten",
"fr": "Comptable fiduciaire - Gérer les données comptables et financières"
- }
+ },
+ "accessRules": [
+ # Full UI access
+ {"context": "UI", "item": None, "view": True},
+ # Group-level DATA access
+ {"context": "DATA", "item": None, "view": True, "read": "g", "create": "g", "update": "g", "delete": "g"},
+ ]
},
{
"roleLabel": "trustee-client",
@@ -127,7 +140,13 @@ TEMPLATE_ROLES = [
"en": "Trustee Client - View own accounting data and documents",
"de": "Treuhand-Kunde - Eigene Buchhaltungsdaten und Dokumente einsehen",
"fr": "Client fiduciaire - Consulter ses propres données comptables et documents"
- }
+ },
+ "accessRules": [
+ # Full UI access
+ {"context": "UI", "item": None, "view": True},
+ # Own records only (MY level)
+ {"context": "DATA", "item": None, "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
+ ]
},
]
@@ -185,9 +204,198 @@ def registerFeature(catalogService) -> bool:
meta=resObj.get("meta")
)
+ # Sync template roles to database (with AccessRules)
+ _syncTemplateRolesToDb()
+
logger.info(f"Feature '{FEATURE_CODE}' registered {len(UI_OBJECTS)} UI objects and {len(RESOURCE_OBJECTS)} resource objects")
return True
except Exception as e:
logger.error(f"Failed to register feature '{FEATURE_CODE}': {e}")
return False
+
+
+def _syncTemplateRolesToDb() -> int:
+ """
+ Sync template roles and their AccessRules to the database.
+ Creates global template roles (mandateId=None) if they don't exist.
+
+ Returns:
+ Number of roles created/updated
+ """
+ try:
+ from modules.interfaces.interfaceDbApp import getRootInterface
+ from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
+
+ rootInterface = getRootInterface()
+ db = rootInterface.db
+
+ # Get existing template roles for this feature
+ existingRoles = db.getRecordset(
+ Role,
+ recordFilter={"featureCode": FEATURE_CODE, "mandateId": None}
+ )
+ existingRoleLabels = {r.get("roleLabel"): r.get("id") for r in existingRoles}
+
+ createdCount = 0
+ for roleTemplate in TEMPLATE_ROLES:
+ roleLabel = roleTemplate["roleLabel"]
+
+ if roleLabel in existingRoleLabels:
+ roleId = existingRoleLabels[roleLabel]
+ logger.debug(f"Template role '{roleLabel}' already exists with ID {roleId}")
+
+ # Ensure AccessRules exist for this role
+ _ensureAccessRulesForRole(db, roleId, roleTemplate.get("accessRules", []))
+ else:
+ # Create new template role
+ newRole = Role(
+ roleLabel=roleLabel,
+ description=roleTemplate.get("description", {}),
+ featureCode=FEATURE_CODE,
+ mandateId=None, # Global template
+ featureInstanceId=None,
+ isSystemRole=False
+ )
+ createdRole = db.recordCreate(Role, newRole.model_dump())
+ roleId = createdRole.get("id")
+
+ # Create AccessRules for this role
+ _ensureAccessRulesForRole(db, roleId, roleTemplate.get("accessRules", []))
+
+ logger.info(f"Created template role '{roleLabel}' with ID {roleId}")
+ createdCount += 1
+
+ if createdCount > 0:
+ logger.info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles")
+
+ # Repair instance-specific roles that are missing AccessRules
+ _repairInstanceRolesAccessRules(db, existingRoleLabels)
+
+ return createdCount
+
+ except Exception as e:
+ logger.error(f"Error syncing template roles for feature '{FEATURE_CODE}': {e}")
+ return 0
+
+
+def _repairInstanceRolesAccessRules(db, templateRoleLabels: Dict[str, str]) -> int:
+ """
+ Repair instance-specific roles by copying AccessRules from their template roles.
+ This ensures instance roles created before AccessRules were defined get updated.
+
+ Args:
+ db: Database connector
+ templateRoleLabels: Dict mapping roleLabel to template role ID
+
+ Returns:
+ Number of instance roles repaired
+ """
+ from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
+
+ repairedCount = 0
+
+ # Get all instance-specific roles for this feature (mandateId is NOT None)
+ allRoles = db.getRecordset(Role, recordFilter={"featureCode": FEATURE_CODE})
+ instanceRoles = [r for r in allRoles if r.get("mandateId") is not None]
+
+ for instanceRole in instanceRoles:
+ roleLabel = instanceRole.get("roleLabel")
+ instanceRoleId = instanceRole.get("id")
+
+ # Find matching template role
+ templateRoleId = templateRoleLabels.get(roleLabel)
+ if not templateRoleId:
+ continue
+
+ # Check if instance role has AccessRules
+ existingRules = db.getRecordset(AccessRule, recordFilter={"roleId": instanceRoleId})
+ if existingRules:
+ continue # Already has rules, skip
+
+ # Copy AccessRules from template role
+ templateRules = db.getRecordset(AccessRule, recordFilter={"roleId": templateRoleId})
+ if not templateRules:
+ continue # Template has no rules
+
+ for rule in templateRules:
+ newRule = AccessRule(
+ roleId=instanceRoleId,
+ context=rule.get("context"),
+ item=rule.get("item"),
+ view=rule.get("view", False),
+ read=rule.get("read"),
+ create=rule.get("create"),
+ update=rule.get("update"),
+ delete=rule.get("delete"),
+ )
+ db.recordCreate(AccessRule, newRule.model_dump())
+
+ logger.info(f"Repaired instance role '{roleLabel}' (ID: {instanceRoleId}): copied {len(templateRules)} AccessRules from template")
+ repairedCount += 1
+
+ if repairedCount > 0:
+ logger.info(f"Feature '{FEATURE_CODE}': Repaired {repairedCount} instance roles with missing AccessRules")
+
+ return repairedCount
+
+
+def _ensureAccessRulesForRole(db, roleId: str, ruleTemplates: List[Dict[str, Any]]) -> int:
+ """
+ Ensure AccessRules exist for a role based on templates.
+
+ Args:
+ db: Database connector
+ roleId: Role ID
+ ruleTemplates: List of rule templates
+
+ Returns:
+ Number of rules created
+ """
+ from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
+
+ # Get existing rules for this role
+ existingRules = db.getRecordset(AccessRule, recordFilter={"roleId": roleId})
+
+ # Create a set of existing rule signatures to avoid duplicates
+ existingSignatures = set()
+ for rule in existingRules:
+ sig = (rule.get("context"), rule.get("item"))
+ existingSignatures.add(sig)
+
+ createdCount = 0
+ for template in ruleTemplates:
+ context = template.get("context", "UI")
+ item = template.get("item")
+ sig = (context, item)
+
+ if sig in existingSignatures:
+ continue
+
+ # Map context string to enum
+ if context == "UI":
+ contextEnum = AccessRuleContext.UI
+ elif context == "DATA":
+ contextEnum = AccessRuleContext.DATA
+ elif context == "RESOURCE":
+ contextEnum = AccessRuleContext.RESOURCE
+ else:
+ contextEnum = context
+
+ newRule = AccessRule(
+ roleId=roleId,
+ context=contextEnum,
+ item=item,
+ view=template.get("view", False),
+ read=template.get("read"),
+ create=template.get("create"),
+ update=template.get("update"),
+ delete=template.get("delete"),
+ )
+ db.recordCreate(AccessRule, newRule.model_dump())
+ createdCount += 1
+
+ if createdCount > 0:
+ logger.debug(f"Created {createdCount} AccessRules for role {roleId}")
+
+ return createdCount
diff --git a/modules/interfaces/interfaceDbChat.py b/modules/interfaces/interfaceDbChat.py
index bbe2d9ce..de16d6af 100644
--- a/modules/interfaces/interfaceDbChat.py
+++ b/modules/interfaces/interfaceDbChat.py
@@ -1729,8 +1729,14 @@ class ChatObjects:
totalPages=totalPages
)
- def getAutomationDefinition(self, automationId: str) -> Optional[AutomationDefinition]:
- """Returns an automation definition by ID if user has access, with computed status."""
+ def getAutomationDefinition(self, automationId: str, includeSystemFields: bool = False) -> Optional[AutomationDefinition]:
+ """Returns an automation definition by ID if user has access, with computed status.
+
+ Args:
+ automationId: ID of the automation to get
+ includeSystemFields: If True, returns raw dict with system fields (_createdBy, etc).
+ If False (default), returns Pydantic model without system fields.
+ """
try:
# Use RBAC filtering
filtered = getRecordsetWithRBAC(self.db,
@@ -1749,6 +1755,16 @@ class ChatObjects:
automation["executionLogs"] = []
# Enrich with user and mandate names
self._enrichAutomationWithUserAndMandate(automation)
+
+ # For internal use (execution), return raw dict with system fields
+ if includeSystemFields:
+ # Return as simple namespace object so getattr works
+ class AutomationWithSystemFields:
+ def __init__(self, data):
+ for key, value in data.items():
+ setattr(self, key, value)
+ return AutomationWithSystemFields(automation)
+
# Clean metadata fields and return Pydantic model
cleanedRecord = {k: v for k, v in automation.items() if not k.startswith("_")}
return AutomationDefinition(**cleanedRecord)
@@ -1771,9 +1787,12 @@ class ChatObjects:
# Ensure database connector has correct userId context
# The connector should have been initialized with userId, but ensure it's updated
- if self.userId and hasattr(self.db, 'updateContext'):
+ if not self.userId:
+ logger.error(f"createAutomationDefinition: userId is not set! Cannot set _createdBy. currentUser={self.currentUser}")
+ elif hasattr(self.db, 'updateContext'):
try:
self.db.updateContext(self.userId)
+ logger.debug(f"createAutomationDefinition: Updated database context with userId={self.userId}")
except Exception as e:
logger.warning(f"Could not update database context: {e}")
diff --git a/modules/routes/routeAdminFeatures.py b/modules/routes/routeAdminFeatures.py
index 575af01e..9cb023c6 100644
--- a/modules/routes/routeAdminFeatures.py
+++ b/modules/routes/routeAdminFeatures.py
@@ -248,7 +248,10 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
recordFilter={"userId": userId, "featureInstanceId": instanceId}
)
+ logger.debug(f"_getInstancePermissions: userId={userId}, instanceId={instanceId}, featureAccesses={len(featureAccesses) if featureAccesses else 0}")
+
if not featureAccesses:
+ logger.debug(f"_getInstancePermissions: No FeatureAccess found for user {userId} and instance {instanceId}")
return permissions
# Get role IDs via FeatureAccessRole junction table
@@ -259,7 +262,10 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
)
roleIds = [far.get("roleId") for far in featureAccessRoles]
+ logger.debug(f"_getInstancePermissions: featureAccessId={featureAccessId}, roleIds={roleIds}")
+
if not roleIds:
+ logger.debug(f"_getInstancePermissions: No roles found for FeatureAccess {featureAccessId}")
return permissions
# Get permissions (AccessRules) for all roles
@@ -269,6 +275,8 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
recordFilter={"roleId": roleId}
)
+ logger.debug(f"_getInstancePermissions: roleId={roleId}, accessRules={len(accessRules) if accessRules else 0}")
+
for rule in accessRules:
context = rule.get("context", "")
item = rule.get("item", "")
@@ -303,8 +311,13 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
# Handle UI context (views)
elif context == "UI" or context == AccessRuleContext.UI:
+ ruleView = rule.get("view", False)
if item:
- permissions["views"][item] = permissions["views"].get(item, False) or rule.get("view", False)
+ # Specific view rule
+ permissions["views"][item] = permissions["views"].get(item, False) or ruleView
+ elif ruleView:
+ # item=None means all views - set a wildcard flag
+ permissions["views"]["_all"] = True
return permissions
diff --git a/modules/shared/attributeUtils.py b/modules/shared/attributeUtils.py
index 5f6c2531..863d7f36 100644
--- a/modules/shared/attributeUtils.py
+++ b/modules/shared/attributeUtils.py
@@ -5,6 +5,7 @@ Shared utilities for model attributes and labels.
"""
from pydantic import BaseModel, Field, ConfigDict
+from pydantic_core import PydanticUndefined
from typing import Dict, Any, List, Type, Optional, Union
import inspect
import importlib
@@ -212,7 +213,8 @@ def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguag
# Don't call it here - it's meant to be called per-instance
# Instead, store a marker that indicates it exists
field_default = None # Frontend should use first option or specific logic
- elif default_value is not ...: # Ellipsis means no default
+ elif default_value is not ... and default_value is not PydanticUndefined:
+ # Ellipsis or PydanticUndefined means no default
# Convert enum to its value if it's an enum
if hasattr(default_value, 'value'):
field_default = default_value.value
diff --git a/modules/workflows/automation/mainWorkflow.py b/modules/workflows/automation/mainWorkflow.py
index 23bbf125..503d1d13 100644
--- a/modules/workflows/automation/mainWorkflow.py
+++ b/modules/workflows/automation/mainWorkflow.py
@@ -76,8 +76,8 @@ async def executeAutomation(automationId: str, services) -> ChatWorkflow:
}
try:
- # 1. Load automation definition
- automation = services.interfaceDbChat.getAutomationDefinition(automationId)
+ # 1. Load automation definition (with system fields for _createdBy access)
+ automation = services.interfaceDbChat.getAutomationDefinition(automationId, includeSystemFields=True)
if not automation:
raise ValueError(f"Automation {automationId} not found")
@@ -105,18 +105,17 @@ async def executeAutomation(automationId: str, services) -> ChatWorkflow:
# 3. Get user who created automation
creatorUserId = getattr(automation, "_createdBy", None)
- # CRITICAL: Automation MUST run as creator user only, or fail
+ # _createdBy is a system attribute - must be present
if not creatorUserId:
errorMsg = f"Automation {automationId} has no creator user (_createdBy field missing). Cannot execute automation."
logger.error(errorMsg)
executionLog["messages"].append(errorMsg)
raise ValueError(errorMsg)
- # Get user from database using services
+ # Get creator user from database
creatorUser = services.interfaceDbApp.getUser(creatorUserId)
if not creatorUser:
raise ValueError(f"Creator user {creatorUserId} not found")
-
executionLog["messages"].append(f"Using creator user: {creatorUserId}")
# 4. Create UserInputRequest from plan
@@ -205,10 +204,17 @@ async def syncAutomationEvents(services, eventUser) -> Dict[str, Any]:
registeredEvents = {}
for automation in filtered:
- automationId = automation.id
- isActive = automation.active if hasattr(automation, 'active') else False
- currentEventId = automation.eventId if hasattr(automation, 'eventId') else None
- schedule = automation.schedule if hasattr(automation, 'schedule') else None
+ # Handle both dict and object access patterns
+ if isinstance(automation, dict):
+ automationId = automation.get('id')
+ isActive = automation.get('active', False)
+ currentEventId = automation.get('eventId')
+ schedule = automation.get('schedule')
+ else:
+ automationId = automation.id
+ isActive = automation.active if hasattr(automation, 'active') else False
+ currentEventId = automation.eventId if hasattr(automation, 'eventId') else None
+ schedule = automation.schedule if hasattr(automation, 'schedule') else None
if not schedule:
logger.warning(f"Automation {automationId} has no schedule, skipping")
@@ -287,8 +293,8 @@ def createAutomationEventHandler(automationId: str, eventUser):
# Get services for event user (provides access to interfaces)
eventServices = getServices(eventUser, None)
- # Load automation using event user context
- automation = eventServices.interfaceDbChat.getAutomationDefinition(automationId)
+ # Load automation using event user context (with system fields for _createdBy access)
+ automation = eventServices.interfaceDbChat.getAutomationDefinition(automationId, includeSystemFields=True)
if not automation or not getattr(automation, "active", False):
logger.warning(f"Automation {automationId} not found or not active, skipping execution")
return