Automation Scripts in Maximo Manage: From Beginner Patterns to Production-Grade Deployments
>
Automation Scripts in Maximo Manage: From Beginner Patterns to Production-Grade Deployments
Automation scripts are the most powerful extensibility mechanism in Maximo Manage. They allow you to inject custom business logic at virtually every touchpoint in the application without writing Java code, compiling EAR files, or restarting application servers. But with great power comes the responsibility to write scripts that are maintainable, performant, and safe for production use.
This deep dive covers the patterns, practices, and pitfalls that separate production-grade automation scripts from the quick-and-dirty scripts that cause performance problems and production incidents.
The Automation Script Architecture
Before writing scripts, you need to understand how the automation script engine works under the hood. The engine is not simply an embedded interpreter. It is a full lifecycle manager that handles script compilation, variable binding, transaction management, and error propagation.
Script Launch Points
Every automation script attaches to a launch point, which defines when the script executes and what variables are available. The launch point types are:
| Launch Point | Trigger | Available Variables | Common Use Cases |
|---|---|---|---|
| Object | MBO events (init, save, delete) | mbo, mboName |
Field validation, defaulting, cross-object updates |
| Attribute | Field-level events | mbo, attributeName |
Field validation, dynamic domain filtering |
| Action | User-initiated action | mbo, params |
Custom button actions, workflow actions |
| Integration | Inbound/outbound message processing | irData, erData |
Data transformation, enrichment |
| Custom Condition | Expression evaluation | mbo |
Conditional UI, workflow branching |
| Escalation | Escalation processing | mbo |
Scheduled business logic |
The Implicit Variable Model
When a script executes, the engine injects a set of implicit variables into the script's scope. Understanding these variables is critical:
# Implicit variables available in object launch point scripts
# mbo: The current MBO (Main Business Object) instance
# mboName: String name of the MBO
# app: String name of the current application (may be None)
# user: String name of the current user
# interactive: Boolean indicating if triggered by UI interaction
# params: Dictionary of parameters (action scripts only)
# ctx: Dictionary of additional context variables
# Example: Logging the implicit context
from java.util import Date
from psdi.util.logging import MXLoggerFactory
logger = MXLoggerFactory.getLogger("maximo.autoscript")
logger.info(f"Script executing for MBO: {mboName}")
logger.info(f"User: {user}, Interactive: {interactive}")
Script Language Selection
Maximo supports both Python (Jython) and JavaScript (Nashorn/GraalVM) for automation scripts. The choice matters:
Python (Jython) is the recommended language for most use cases. It has better integration with Maximo's Java APIs, more readable syntax for business logic, and a larger community of Maximo-specific examples. Jython runs on the JVM, which means you can import and use any Java class directly.
JavaScript is useful when you need to share logic with frontend code or when your team has stronger JavaScript skills. However, the Nashorn engine has been deprecated in newer JDK versions, and IBM is transitioning to GraalVM JavaScript. Check your JDK version before committing to JavaScript.
# Python (Jython) - Recommended
from psdi.mbo import MboConstants
if mbo.isNull("ASSETNUM"):
mbo.setValue("ASSETNUM", mbo.getMboValue("ASSETNUM").getSequenceValue())
# JavaScript - Alternative
if (mbo.isNull("ASSETNUM")) {
mbo.setValue("ASSETNUM", mbo.getMboValue("ASSETNUM").getSequenceValue());
}
Production Pattern 1: Defensive Field Validation
The most common automation script pattern is field validation. Here is a production-grade pattern that handles edge cases properly:
"""
Object Launch Point: WOCHANGE
Event: Save (Before Save)
Purpose: Validate that high-priority work orders have a supervisor assigned
"""
from psdi.mbo import MboConstants
from psdi.util import MXApplicationException
def validate_priority_supervisor():
# Guard clause: Only validate on specific conditions
priority = mbo.getString("WOPRIORITY")
if priority is None or priority not in ("1", "2"):
return # Not a high-priority WO, skip validation
# Guard clause: Skip if status doesn't warrant validation
status = mbo.getString("STATUS")
if status in ("WAPPR", "COMP", "CLOSE"):
return # Approved or completed WOs don't need this check
# Check if supervisor is assigned
supervisor = mbo.getString("SUPERVISOR")
if supervisor is None or supervisor.strip() == "":
# Get the MboValue for proper error attachment
supervisor_mv = mbo.getMboValue("SUPERVISOR")
raise MXApplicationException(
"workorder",
"highPriorityRequiresSupervisor",
[priority]
)
# Execute validation
validate_priority_supervisor()
Key patterns in this script:
- Guard clauses return early when validation is not needed, keeping the main logic flat and readable.
- Explicit status checks prevent validation from firing during bulk operations or data loads.
- MXApplicationException with message keys allows for internationalized error messages.
- MboValue reference ensures the error is attached to the correct field in the UI.
Production Pattern 2: Cross-Object Updates with Transaction Safety
Scripts that modify related objects must handle transaction boundaries correctly. A common mistake is modifying objects without considering what happens on rollback:
"""
Object Launch Point: ASSET
Event: Save (After Save)
Purpose: Update related work orders when asset criticality changes
"""
from psdi.mbo import MboConstants
from psdi.mbo import MboSetRemote
from psdi.server import MXServer
def update_related_work_orders():
# Only proceed if criticality actually changed
if not mbo.isModified("CRITICALITY"):
return
new_criticality = mbo.getString("CRITICALITY")
assetnum = mbo.getString("ASSETNUM")
# Build relationship query for open work orders
wo_set = mbo.getMboSet("OPENWORKORDERS")
wo_set.setWhere("ASSETNUM = :1 AND STATUS NOT IN ('COMP', 'CLOSE', 'CAN')")
wo_set.setWhereParams([assetnum])
wo_set.reset()
try:
wo = wo_set.moveFirst()
while wo is not None:
# Set the criticality on the work order
wo.setValue("ASSETCRITICALITY", new_criticality, MboConstants.NOACCESSCHECK)
# Add a work log entry documenting the change
wl_set = wo.getMboSet("WORKLOG")
wl = wl_set.add()
wl.setValue("DESCRIPTION",
f"Asset criticality updated to {new_criticality}. Automatically propagated.",
MboConstants.NOACCESSCHECK)
wl.setValue("LOGTYPE", "UPDATE", MboConstants.NOACCESSCHECK)
wo = wo_set.moveNext()
finally:
wo_set.close()
# Execute
update_related_work_orders()
Critical patterns here:
isModified()check prevents unnecessary execution when the field has not changed.NOACCESSCHECKflag bypasses security checks for system-initiated updates, but use it carefully.try/finallywithclose()prevents MBO set leaks that cause memory issues.- Relationship-based queries are preferred over direct
MXServer.getMboSet()for better performance and data scoping.
Production Pattern 3: Integration Scripts for Data Transformation
Integration scripts process inbound and outbound messages. They are the bridge between Maximo's internal data model and external system formats:
"""
Integration Launch Point: MXASSETInterface
Direction: Inbound (Before Processing)
Purpose: Transform external asset data to Maximo format
"""
from java.util import HashMap
from java.text import SimpleDateFormat
from java.util import Date
def transform_inbound_asset():
# Access the integration data structure
ir_data = irData
# Transform external date format to Maximo format
ext_install_date = ir_data.getCurrentData("INSTALLDATE")
if ext_install_date is not None and ext_install_date.strip() != "":
try:
# External format: MM/DD/YYYY
ext_format = SimpleDateFormat("MM/dd/yyyy")
maximo_format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
parsed_date = ext_format.parse(ext_install_date)
ir_data.setCurrentData("INSTALLDATE", maximo_format.format(parsed_date))
except Exception as e:
logger.error(f"Date parse error for INSTALLDATE: {ext_install_date}")
ir_data.setCurrentData("INSTALLDATE", None)
# Map external status codes to Maximo internal values
status_map = {
"ACT": "OPERATING",
"INACT": "NOT READY",
"DECOMM": "DECOMMISSIONED",
"MOTH": "DECOMMISSIONED"
}
ext_status = ir_data.getCurrentData("STATUS")
if ext_status is not None:
maximo_status = status_map.get(ext_status.upper(), "NOT READY")
ir_data.setCurrentData("STATUS", maximo_status)
# Enrich with default values for missing fields
if ir_data.getCurrentData("SITEID") is None:
ir_data.setCurrentData("SITEID", "BEDFORD")
if ir_data.getCurrentData("ORGID") is None:
ir_data.setCurrentData("ORGID", "EAGLE")
# Execute
transform_inbound_asset()
Production Pattern 4: Scheduled Logic with Escalations
Escalation scripts run on a schedule and are ideal for batch processing, notifications, and periodic data maintenance:
"""
Escalation Launch Point: PMDUEDATECHECK
Purpose: Identify PMs due within 7 days and create notification records
"""
from psdi.server import MXServer
from psdi.mbo import MboConstants
from java.util import Calendar, Date
from java.text import SimpleDateFormat
def check_due_pms():
# Calculate the date 7 days from now
cal = Calendar.getInstance()
cal.add(Calendar.DAY_OF_YEAR, 7)
target_date = cal.getTime()
# Get PM records due within 7 days
pm_set = MXServer.getMXServer().getMboSet(
"PM", mbo.getUserInfo()
)
pm_set.setWhere("STATUS = 'ACTIVE' AND NEXTDUEDATE <= :1")
pm_set.setWhereParams([target_date])
try:
pm = pm_set.moveFirst()
count = 0
while pm is not None:
# Create a notification or escalation record
# This is a simplified example; in production you would
# use the COMMUNICATION object for actual notifications
pm.setValue("ALERTSTATUS", "DUE_SOON", MboConstants.NOACCESSCHECK)
count += 1
pm = pm_set.moveNext()
logger.info(f"PM Due Check: Updated {count} PM records with DUE_SOON status")
finally:
pm_set.close()
# Execute
check_due_pms()
Performance Optimization for Automation Scripts
Poorly written automation scripts are the leading cause of performance degradation in Maximo Manage. Here are the rules:
Rule 1: Minimize MBO Set Operations
Every getMboSet() call creates a new database connection and executes a query. In a loop, this is catastrophic:
# BAD: N+1 query problem
wo_set = mbo.getMboSet("ALLWORKORDERS")
wo = wo_set.moveFirst()
while wo is not None:
# This executes a new query for every work order
asset = wo.getMboSet("ASSET")
# ... process asset
wo = wo_set.moveNext()
# GOOD: Use relationships or batch queries
wo_set = mbo.getMboSet("ALLWORKORDERS")
wo_set.setFlag(MboConstants.ALWAYSLOADIN, True) # Load related data eagerly
wo = wo_set.moveFirst()
while wo is not None:
# Access related data through the already-loaded relationship
asset = wo.getRelatedMbo("ASSET")
# ... process asset
wo = wo_set.moveNext()
Rule 2: Use setWhere() with Parameters
Never concatenate values into SQL strings. Always use parameterized queries:
# BAD: SQL injection risk and poor performance
wo_set.setWhere("ASSETNUM = '" + assetnum + "'")
# GOOD: Parameterized query
wo_set.setWhere("ASSETNUM = :1")
wo_set.setWhereParams([assetnum])
Rule 3: Limit Result Sets
Always add row limits to queries that could return large result sets:
# Limit to 500 rows to prevent memory issues
wo_set.setLimit(500)
Rule 4: Avoid UI Operations in Background Scripts
Scripts triggered by escalations, integrations, or cron tasks have no UI context. Calling UI-related methods will fail or cause unexpected behavior:
# BAD: This will fail in escalation scripts
from psdi.webclient.system.controller import WebClientEvent
# WebClientEvent is not available outside the UI context
# GOOD: Use server-side APIs only
from psdi.server import MXServer
Testing Automation Scripts
Testing automation scripts is challenging because they depend on the Maximo runtime. Here is a practical testing strategy:
Level 1: Syntax Validation
The simplest test: save the script and check for compilation errors. Maximo validates syntax on save.
Level 2: Unit Testing with Test Data
Create a dedicated test site and test assets. Run scripts against known data and verify the results:
# Test script: Verify validation logic
def test_priority_validation():
# Create a test work order
wo_set = MXServer.getMXServer().getMboSet("WORKORDER", userInfo)
wo = wo_set.add()
wo.setValue("WOPRIORITY", "1")
wo.setValue("DESCRIPTION", "TEST - Automation Script Validation")
# Intentionally leave SUPERVISOR blank
try:
wo_set.save()
print("FAIL: Should have thrown exception for missing supervisor")
except MXApplicationException as e:
print(f"PASS: Validation caught missing supervisor: {e.getMessage()}")
finally:
wo_set.close()
Level 3: Logging for Debugging
Add strategic logging to understand script behavior in production:
# Use different log levels for different purposes
logger.debug(f"Script entry: mbo={mboName}, user={user}") # Development only
logger.info(f"Processed {count} records") # Production monitoring
logger.warn(f"Unexpected status: {status}") # Anomaly detection
logger.error(f"Failed to process record: {wonum}", exc_info=True) # Error tracking
Practical Implications
For Maximo developers: Automation scripts are now the primary extension mechanism. Java customizations should be reserved for cases where automation scripts cannot achieve the required functionality. Invest time in learning the automation script API thoroughly, including the less commonly used features like script caching, custom conditions, and the ctx variable for passing context between chained scripts.
For system administrators: Monitor automation script performance. Scripts that run on every save of a heavily used object (like WORKORDER or ASSET) can have outsized performance impact. Use Maximo's built-in logging to identify slow-running scripts and work with developers to optimize them.
For solution architects: Design your automation script library with reusability in mind. Create a library of utility functions that can be shared across scripts. Use Maximo's script include feature to avoid duplicating common logic. Establish naming conventions and documentation standards for all automation scripts.
For QA teams: Automation scripts need their own test cases. Every script should have documented expected behavior, edge cases, and failure modes. Test scripts with bulk data loads to ensure they perform acceptably under production volumes.
Bottom Line
Automation scripts are the future of Maximo customization. They are more maintainable than Java customizations, easier to deploy, and sufficient for the vast majority of business logic requirements. But they are not magic. Poorly written automation scripts cause performance problems, data corruption, and production incidents just as effectively as poorly written Java code.
The patterns in this article represent battle-tested approaches used in production Maximo deployments. Follow them, test thoroughly, monitor performance, and your automation scripts will be an asset rather than a liability. Ignore them, and you will eventually find yourself debugging a script that runs 50,000 times per hour and takes 3 seconds per execution.