Automation Scripts in Maximo Manage: From Beginner Patterns to Production-Grade Deployments

>

Share

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:

  1. Guard clauses return early when validation is not needed, keeping the main logic flat and readable.
  2. Explicit status checks prevent validation from firing during bulk operations or data loads.
  3. MXApplicationException with message keys allows for internationalized error messages.
  4. 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:

  1. isModified() check prevents unnecessary execution when the field has not changed.
  2. NOACCESSCHECK flag bypasses security checks for system-initiated updates, but use it carefully.
  3. try/finally with close() prevents MBO set leaks that cause memory issues.
  4. 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.

Read more