Automation Scripts in Maximo Manage 9.1: A Practitioner's Guide to Exception Handling, Complex Conditions, and Custom Launch Points
A hands-on deep dive into the three automation script patterns that quietly break Maximo Manage in production: silent exception swallowing, SQL-only workflow conditions, and launch point scoping. Includes code examples, comparison tables, and a real failure scenario.
Automation Scripts in Maximo Manage 9.1: A Practitioner's Guide to Exception Handling, Complex Conditions, and Custom Launch Points
If you have run Maximo Manage in production for any length of time, you have lived through the moment when a workflow does the wrong thing, or does nothing, and the only trace is a log line that does not actually tell you what went wrong. The most common root cause is not Maximo. The most common root cause is an automation script that swallowed an exception, an SQL condition that could not express the business rule, or a launch point that fired on the wrong object event and quietly updated the wrong record. Bruno Portaluri's recent community writeups on complex workflow condition scripts and on proper exception handling in Jython have struck a nerve for exactly that reason. They describe the patterns that decide whether your automation layer is reliable or a liability.
This article is a practitioner's deep dive into those patterns. It is written for the Maximo developer or senior administrator who already knows how to create an automation script, attach a launch point, and write a basic Jython snippet. The goal is to move beyond the basics and into the patterns that hold up in production: how to structure exception handling so that a failure is visible and recoverable, how to combine SQL conditions with custom condition scripts when neither alone is enough, and how to think about launch point scoping and ordering when one object event triggers multiple scripts.
The Three Failure Modes That Quietly Break Production
Most automation script problems in Maximo Manage fall into one of three categories. The first is silent exception swallowing. A Jython block calls an MBO method, the method throws, the script catches broadly, logs at WARN or INFO, and returns a default value. The workflow continues, the user sees the wrong screen, and nobody notices until someone asks why three work orders have a status of "COMPW" with no labor records. The second is SQL-only workflow conditions. A condition is built with a SQL expression that works fine on the first 100 records and then degrades to a table scan as the data grows, or that cannot express the rule at all and silently always returns false. The third is launch point scoping. A script fires on the wrong object, on the wrong event, or in the wrong order relative to a second script, and the combined effect is different from what either script does in isolation.
The good news is that all three failure modes are fixable with patterns that are well-known in the broader scripting community and that translate cleanly to Maximo. The bad news is that they require a level of discipline that is easy to skip when the deadline is close and the script is "just a small one." The rest of this article walks through the patterns that make those small scripts reliable, with code examples that you can adapt to your own environment.
Exception Handling That Actually Surfaces Failure
The most important rule for automation scripts is that an uncaught exception in an automation script is almost always better than a caught one. Maximo's automation script framework already wraps your script in a try/except that logs the failure to the system log and prevents the script from corrupting the MBO state. If you catch an exception, log it, and return a default value, you are explicitly choosing to hide the failure from the framework, and you are on the hook for making sure the default value is correct in every case. Most silent failures in production come from the case where the default value is correct most of the time and wrong in exactly the situations that matter.
The pattern that works is straightforward. At the top of your script, let exceptions propagate unless you can do something useful with them. If you can do something useful, scope the try/except to the smallest possible block, re-raise with context, and never return a default that masks the original error. The example below shows a launch point script that updates a related record and includes both the wrong and the right way to handle a missing record.
# WRONG: silently swallows the failure
try:
wo = mbo.getMboSet("RELATEDWOS").getMbo(0)
wo.setValue("description", "Linked to " + mbo.getString("wonum"))
wo.setValue("status", "PENDING")
except Exception as e:
logger.warn("Could not update related WO: " + str(e))
# workflow continues, record is not updated, no one knows
# RIGHT: lets the framework handle it, with context
woSet = mbo.getMboSet("RELATEDWOS")
if woSet.isEmpty():
# explicit decision: no related work, nothing to do
pass
else:
wo = woSet.getMbo(0)
wo.setValue("description", "Linked to " + mbo.getString("wonum"))
wo.setValue("status", "PENDING")
# any unexpected failure (network, MBO state, locking) propagates
# and is logged by the framework with full stack trace
The first version looks defensive. In practice it hides a missing relationship, a stale cache, a lock conflict, or a script that was attached to the wrong object. The second version is explicit about the no-op case and lets every other failure surface. The Maximo log will show the full stack trace, the workflow will fail cleanly, and the operator will see that the work order did not move. That is what you want.
When you do need to catch an exception, the right pattern is to catch the most specific exception you can, do something useful with the context, and re-raise. The example below shows a script that calls an external system via REST and needs to translate a specific HTTP error code into a workflow decision, but should let every other error propagate.
import json
from com.ibm.json.java import JSONObject
# Assume a helper that does the HTTP call
def call_external_system(payload):
# returns (status_code, body)
pass
try:
payload = json.dumps({"wonum": mbo.getString("wonum")})
status, body = call_external_system(payload)
if status == 409:
# conflict is expected in some cases; record and continue
mbo.setValue("description", mbo.getString("description") + " [ext conflict]")
elif status >= 200 and status < 300:
data = JSONObject(body)
mbo.setValue("externalref", data.get("id"))
else:
# unexpected status; re-raise with context
raise RuntimeError("External system returned status " + str(status))
except java.io.IOException as e:
# network problem; re-raise with the original cause
raise RuntimeError("Network call failed: " + str(e), e)
The pattern is "catch what you can handle, re-raise what you cannot." It is not new, but it is the pattern that is most often violated in Maximo automation scripts.
Complex Workflow Conditions: When SQL Is Not Enough
The second failure mode is the SQL-only workflow condition. Maximo's workflow designer lets you attach a condition expression to a node, and that condition can be a SQL query against the database, an in-memory JavaScript-like expression, or a reference to a custom condition class. The SQL option is the default and is often the right choice, but it has two sharp edges.
The first sharp edge is performance. A SQL condition that joins across multiple tables, or that calls a function, will be evaluated every time the workflow reaches the node. If the underlying tables are large, the workflow will slow down. The standard mitigation is to ensure that the columns referenced in the condition are indexed, and to keep the SQL expression as narrow as possible. The second sharp edge is expressiveness. SQL cannot easily express a rule that depends on the value of an attribute on a related MBO, on a comparison against a value that is computed at runtime, or on a business rule that lives in a Java class.
The custom condition launch point is the right tool for those cases. A custom condition script is a Jython script that receives the MBO and returns a boolean. It can call any of the standard Maximo APIs, including MboSet, Mbo, and the system utilities, and it can implement arbitrarily complex logic. The pattern is to use a SQL condition when the rule is simple, fast, and expressible in SQL, and to fall back to a custom condition when the rule is complex, slow in SQL, or requires access to related MBOs.
The example below shows a workflow condition that decides whether to route a work order to an approval node based on the combination of the priority, the cost, and the existence of an open safety incident. The SQL version would be ugly and would not be able to do the open-incident check without a join. The custom condition is straightforward.
# Custom condition launch point on WORKORDER object
# Returns True if the work order needs safety review
priority = mbo.getInt("priority")
estcost = mbo.getDouble("estcost")
asset = mbo.getString("assetnum")
# rule 1: any priority 1 work order on a critical asset
if priority == 1:
assetSet = mbo.getMboSet("$ASSET", "ASSET", "assetnum = :1 and criticality = 'CRIT'", [asset])
if not assetSet.isEmpty():
return True
# rule 2: any work order with estimated cost above the threshold
if estcost > 50000.0:
return True
# rule 3: any work order on an asset that has an open safety incident
if asset is not None:
incSet = mbo.getMboSet("$INC",
"INCIDENT",
"assetnum = :1 and status in ('NEW','INPROG')",
[asset])
if not incSet.isEmpty():
return True
return False
The pattern to remember is that the SQL condition is a filter, and the custom condition is a function. Filters are fast when the underlying data is structured and indexed. Functions are the right tool when the rule needs to traverse relationships or compute a value. The mistake is to use a function when a filter would do, or to use a filter when the rule is too complex for SQL.
Launch Point Scoping and Ordering
The third failure mode is launch point scoping, and it is the one that causes the most subtle production bugs. An automation script is associated with an object, an event, and an attribute. The object determines which MBO the script runs against, the event determines when the script runs (init, save, validate, custom event), and the attribute optionally narrows the trigger to a specific attribute change. If any of those three are wrong, the script either does not fire when it should, or fires when it should not.
The most common mistake is to attach a save launch point to an object that does not actually change the attribute the script is meant to react to. For example, a script intended to update a work order's status when its associated asset's location changes will not fire from a save launch point on WORKORDER, because the asset is not being saved as part of the work order save. The script needs to be attached to the ASSET object, or to fire from an attribute launch point on the work order's assetnum attribute, or to be invoked explicitly from a separate script.
The second most common mistake is to attach two scripts to the same launch point in the wrong order. Maximo runs automation scripts in the order they appear in the launch point configuration, and later scripts see the changes made by earlier scripts. If script A updates a status to APPR and script B reads the status to decide whether to set a related flag, the order matters. The standard mitigation is to keep the scripts small, to name them clearly (the order is determined by the script name, so name them with a numeric prefix if order matters), and to document the dependency in the script header.
The third most common mistake is to use a save launch point for validation that should be a validate launch point. A save launch point runs after the user has clicked Save; a validate launch point runs before. Validation that prevents a save should be a validate launch point; logic that reacts to a save should be a save launch point. Putting validation in a save launch point means the user has already saved the bad data and the script has to either fix it silently or throw a confusing error.
The pattern that works is to design the launch point around the question you are trying to answer. "Did this attribute just change?" is an attribute launch point. "Is this MBO about to be saved in a valid state?" is a validate launch point. "What should happen as a result of this save?" is a save launch point. "What should happen when this object is loaded?" is an init launch point. Most production problems with launch points come from using the wrong type of launch point, and the right fix is to ask which question the script is actually trying to answer.
A Production Failure Scenario
The patterns above are easier to evaluate in the context of a real failure. Consider a recent production scenario at a mid-sized utility. The team had a workflow that routed high-priority work orders to a safety review node, and the custom condition was a SQL expression that joined WORKORDER to ASSET and checked for a flag on the asset. The expression worked in development, worked in QA, and worked for the first 200 production records. Then the asset table grew past a million rows and the join started taking 8 to 12 seconds per evaluation. The workflow queue backed up. The team noticed because the cron that processed the workflow queue started timing out.
The first attempt at a fix was to add an index on the asset flag column. The index helped for the simple join, but the expression also referenced a work order attribute that was not indexed, so the plan still included a table scan. The second attempt was to rewrite the condition as a custom condition script. The script read the assetnum from the MBO, queried the asset MboSet for the flag, and returned true or false. The custom condition was evaluated per work order, but the MboSet query was fast because the assetnum is the primary key.
The lesson is not that custom conditions are always faster than SQL conditions. The lesson is that the right tool depends on the data shape, and that you should know the cost of the condition before you deploy it. The team added a monitoring metric for workflow condition evaluation time, set a threshold of 500 milliseconds, and configured an alert. They also documented the rule in a way that the next administrator would know to use a custom condition for that specific check.
Practical Implications
The patterns in this article are not specific to any particular version of Maximo Manage. They have been true since the original automation script framework was introduced, and they remain true in MAS 9.1 and will be true in MAS 9.2. What is new is the operational context. MAS 9.2 exposes automation scripts through MCP and through the AI Service skill runtime, which means the cost of a silent failure is higher: an agent may make a decision based on a script that silently returned the wrong value, and the agent's action will be wrong. The discipline of explicit, observable, recoverable automation scripts is no longer just good practice; it is a prerequisite for trustworthy AI.
For teams planning the 9.2 upgrade, the practical implication is to invest in script quality now. Audit the existing automation script library for silent exception handling, for SQL conditions that are likely to be slow on production data volumes, and for launch point scoping that depends on undocumented order. The audit will surface a backlog of small improvements that, taken together, will make the 9.2 environment substantially more reliable.
Bottom Line
Automation scripts are the configuration layer that makes Maximo Manage your Maximo Manage. They are also the layer that quietly breaks when the patterns are wrong. The three patterns that decide whether your automation layer is reliable or a liability are exception handling, condition design, and launch point scoping. Get those right, and the rest of the configuration work is straightforward. Get them wrong, and no amount of UI polish or AI capability will save you. The good news is that the patterns are not hard to learn, the cost of getting them right is low, and the operational payoff is high.