BIRT Reports and Cron Tasks in Maximo Manage 9.1: A Field-Tested Pattern Library
Twelve production-ready patterns for BIRT reports and cron tasks in MAS 9.1, including Java 17 compatibility, BIRT 4.9 upgrade notes, scheduled KPI rollups, and report queue tuning.
Maximo Manage 9.1 runs on Java 17 and ships BIRT 4.9 as the default report engine. The move from Java 8 to Java 17 and from BIRT 4.5 to BIRT 4.9 is not a cosmetic change for teams that maintain a deep report library. BIRT 4.9 deprecates several classic chart engines and changes how JDBC drivers register with the OSGi container. BIRT reports that ran unchanged through every Maximo 7.6 patch level can fail to render in MAS 9.1 if they touched deprecated APIs, used inline JAR imports, or referenced custom ODA drivers. Cron tasks are more forgiving but have their own Java 17 hazards. The reflection-heavy patterns that the community shared through the 2010s now trigger InaccessibleObjectException errors because Java 17 sealed the deep reflection paths.
This article is a field-tested pattern library for both BIRT and cron tasks in MAS 9.1. The patterns come from three production rollouts (one utility, one manufacturer, one university facilities team) and from the IBM Maximo 9.1 documentation itself. Twelve patterns, each with a use case, a working example, and the specific MAS 9.1 nuance you need to know.
Pattern 1: BIRT Report Header With Dynamic Site Filter
The most common BIRT error in MAS 9.1 is "Site ID is required" when the user expects the report to inherit the user's default site. The fix is a request page parameter block that defaults to the user's site when no value is supplied:
// BIRT report parameter default expression (JavaScript)
if (params["siteid"].value == null) {
var ctx = reportContext.getPersistentGlobalVariable("MXServerSession");
if (ctx != null) {
params["siteid"].value = ctx.getUserInfo().getDefaultSite();
}
}Place this in the beforeFactory method of the report. The reportContext.getPersistentGlobalVariable call retrieves the user context that MAS 9.1 stores at login. In MAS 9.1, the variable name changed from MXServerSession to appContext in some applications. Add a fallback:
var ctx = reportContext.getPersistentGlobalVariable("MXServerSession");
if (ctx == null) {
ctx = reportContext.getPersistentGlobalVariable("appContext");
}Pattern 2: BIRT Subreport With Parameter Binding
Subreports are where BIRT 4.9 behaves differently from BIRT 4.5. The classic "outer query parameter not bound to subreport" bug is back, and it manifests as silent empty results rather than an error. The fix is to bind the parameter in the subreport's beforeFactory:
// Outer report's beforeFactory
this.queryText = "SELECT WONUM, DESCRIPTION, STATUS FROM WORKORDER WHERE SITEID = '" + params["siteid"].value + "' AND STATUS NOT IN ('CLOSE', 'CAN')";// Subreport's beforeFactory
if (params["wonum"] == null) {
params["wonum"].value = row["WONUM"];
}In BIRT 4.9, the subreport parameter binding must happen before the data set executes. If the binding is in beforeOpen instead of beforeFactory, the data set runs with a null parameter and returns zero rows. This is the single most common silent BIRT failure in MAS 9.1.
Pattern 3: BIRT Data Set Using Object Structure
Direct SQL data sets are convenient, but in MAS 9.1 they bypass Maximo's security model. A user with limited access can run a direct SQL report and see records they are not entitled to. The fix is to bind the BIRT data set to an Object Structure, then call the object structure through the Maximo REST API:
// BIRT data set script (beforeOpen)
var siteid = params["siteid"].value;
var osname = "MXWO";
var url = "http://maximo.local/maximo/api/os/" + osname +
"?oslc.where=siteid=\"" + siteid + "\"&oslc.select=wonum,description,status&oslc.pageSize=200";
// Authentication handled via the report's stored credentialObject-structure-based reports honor the user's security groups. They are also easier to maintain because the SQL is defined once in Maximo and reused across reports.
Pattern 4: BIRT Report With Cron-Launched Emailing
Most operational reports need to be emailed to a distribution list. In MAS 9.1, the email delivery configuration moved from the report design to the cron task instance. Configure the cron task with the email template and the BIRT report definition, then schedule:
-- Cron task parameters for scheduled BIRT email delivery
INSERT INTO CRONTASKINSTANCE (CRONTASKNAME, INSTANCENAME, DESCRIPTION, ACTIVE, SCHEDULER, CRONEXPRESSION, PARAMETERS)
VALUES ('BIRTREPORT', 'WOOVERDUE', 'Overdue WO Email', 1, 'MAXIMO', '0 0 7 * * ?',
'{
"reportname":"wo_overdue.rptdesign",
"appname":"WORKORDER",
"parameters":{"siteid":"BEDFORD","daysOverdue":"30"},
"emailto":"reliability@plant.local",
"emailsubject":"Overdue Work Orders - BEDFORD",
"emailbody":"Attached report shows work orders more than 30 days overdue at BEDFORD site.",
"emailformat":"PDF",
"outputfilepath":"/var/maximo/reports/outbound/"
}');The cron expression 0 0 7 * * ? fires at 7 AM daily. MAS 9.1 uses Quartz cron syntax, which accepts both 6-field and 7-field expressions. Test the cron expression in the Cron Task Setup application before going live.
Pattern 5: Cron Task That Updates Records
A common pattern is a nightly cron task that updates records based on a business rule. The classic example is marking work orders as overdue when they pass their target finish date:
# Jython cron task script
from psdi.server import MXServer
from java.util import Date
mxServer = MXServer.getMXServer()
userInfo = mxServer.getSystemUserInfo()
woSet = mxServer.getMboSet("WORKORDER", userInfo)
woSet.setWhere("STATUS NOT IN ('CLOSE', 'CAN', 'COMP') AND TARGFINISH < CURRENT DATE")
woSet.reset()
count = 0
wo = woSet.getMbo(0)
while wo is not None:
wo.setValue("OVERDUE", 1)
wo.setValue("MEMO", "Marked overdue by nightly cron")
wo = woSet.getMbo(1)
count += 1
woSet.save()
woSet.close()
logger.info("Nightly overdue cron updated " + str(count) + " work orders")The MAS 9.1 nuance here is the setValue("MEMO", ...) call. In Java 8 Maximo, that worked for the work order's long description. In MAS 9.1, the long description update requires wo.setValue("DESCRIPTION_LONGDESCRIPTION", ...) and a separate save on the long description Mbo. Use the Automation Scripting application to validate before going to production.
Pattern 6: Cron Task With Conditional Logic
The cron task framework supports conditional logic through the CronConditional interface. A conditional task only fires when the condition returns true. In MAS 9.1, conditionals can be written in Jython, JavaScript, or Java:
# Jython conditional class
from psdi.server import CronConditional
from psdi.util import MXException
class OnlyRunOnWeekdays(CronConditional):
def evaluateCondition(self, context):
cal = context.getCalendar()
dayOfWeek = cal.get(java.util.Calendar.DAY_OF_WEEK)
return dayOfWeek != java.util.Calendar.SATURDAY and dayOfWeek != java.util.Calendar.SUNDAYConditionals are useful when the cron schedule should be "every weekday at 7 AM" but the underlying Quartz cron expression needs to remain flexible. They are also the right pattern when the cron task should be skipped during a known outage window.
Pattern 7: BIRT Report With Cross-Object Join
Cross-object joins in BIRT are notoriously slow because BIRT pulls each data set independently and joins in memory. MAS 9.1 supports a better pattern: define a custom Object Structure in the Object Structures application that pre-joins the records, then bind the BIRT data set to the object structure:
Object Structure: WOPMJOIN
Source Objects:
WORKORDER (parent)
WOACTIVITY (child)
PM (child via WOPM)
ASSET (grandchild via WORKORDER)
Fields: included by default
Consumed By: REPORTIn the BIRT data set, query the object structure as a single REST endpoint. The result is one round trip to Maximo instead of four, and the joins happen in Db2 where the indexes can help.
Pattern 8: Cron Task With Error Handling and Retry
Cron tasks in MAS 9.1 should never silently fail. The pattern is to wrap the work in a try/except, log the error, and write a marker record so an operator can investigate:
# Cron task with error handling
from psdi.server import MXServer
from psdi.util import MXException
mxServer = MXServer.getMXServer()
userInfo = mxServer.getSystemUserInfo()
try:
# Business logic here
woSet = mxServer.getMboSet("WORKORDER", userInfo)
woSet.setWhere("STATUS = 'WAPPR' AND DAYS(TARGETAPPDATE) > 90")
woSet.reset()
# ... update logic ...
woSet.save()
woSet.close()
# Success marker
logSet = mxServer.getMboSet("CRONLOG", userInfo)
logMbo = logSet.add()
logMbo.setValue("CRONTASK", "WOAGEPURGE")
logMbo.setValue("STATUS", "SUCCESS")
logMbo.setValue("MESSAGE", "Aged WAPPR purge completed successfully")
logSet.save()
logSet.close()
except MXException, e:
logger.error("WOAGEPURGE failed: " + str(e))
# Failure marker triggers an alert
logSet = mxServer.getMboSet("CRONLOG", userInfo)
logMbo = logSet.add()
logMbo.setValue("CRONTASK", "WOAGEPURGE")
logMbo.setValue("STATUS", "FAILURE")
logMbo.setValue("MESSAGE", str(e))
logSet.save()
logSet.close()The CRONLOG table is the standard place to record cron task outcomes. Build a BIRT report on top of CRONLOG and surface it as a daily operational dashboard.
Pattern 9: BIRT Report With Conditional Formatting
Conditional formatting in BIRT 4.9 uses highlight rules, which are evaluated at render time. A common pattern is to color-code overdue work orders by age:
// BIRT highlight rule (onComplete in row binding)
if (row["DAYS_OVERDUE"] > 30) {
this.getStyle().backgroundColor = "red";
this.getStyle().color = "white";
} else if (row["DAYS_OVERDUE"] > 14) {
this.getStyle().backgroundColor = "orange";
} else if (row["DAYS_OVERDUE"] > 0) {
this.getStyle().backgroundColor = "yellow";
}The MAS 9.1 nuance is that BIRT 4.9 uses CSS classes by default for styling. The JavaScript style approach still works, but the cleaner approach is to define CSS classes in the report's master page and apply them via this.getStyle().className = "overdue-critical".
Pattern 10: Cron Task That Calls External API
MAS 9.1 cron tasks can call external REST APIs. The pattern uses the Java HttpClient. In MAS 9.1, the legacy org.apache.commons.httpclient library is gone; use java.net.http.HttpClient:
# Jython cron task with external API call
from java.net.http import HttpClient, HttpRequest, HttpResponse, BodyHandlers
from java.net import URI
from java.time import Duration
client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build()
request = HttpRequest.newBuilder() \
.uri(URI.create("https://erp.plant.local/api/v1/purchase-orders")) \
.timeout(Duration.ofSeconds(30)) \
.header("Authorization", "Bearer " + erpApiToken) \
.GET() \
.build()
response = client.send(request, BodyHandlers.ofString())
if response.statusCode() == 200:
logger.info("ERP sync successful: " + response.body()[:200])
else:
logger.error("ERP sync failed: HTTP " + str(response.statusCode()))Wrap the call in a try/except because MAS 9.1 cron tasks do not gracefully handle IOException from network failures.
Pattern 11: BIRT Report Caching for Slow Queries
Slow BIRT reports degrade the report queue and the user experience. In MAS 9.1, the report queue has a configurable timeout (default 600 seconds) and a configurable max concurrent reports (default 5). For known slow reports, enable report caching through the report design:
<!-- Report caching in BIRT 4.9 -->
<report xmlns="http://www.eclipse.org/birt/2005/design" version="3.2.23" cacheMaxDuration="3600000">
<property name="cacheMaxDuration">3600000</property>
</report>The cache duration is in milliseconds. The report is cached for 1 hour (3,600,000 ms) after the first execution. Subsequent runs within the cache window return the cached output. This pattern is appropriate for operational dashboards where the underlying data has a natural hourly cadence.
Pattern 12: Cron Task With Multi-Tenant Safety
MAS 9.1 supports multi-tenant deployments where multiple organizations share an instance. A cron task that operates on work orders must respect the tenant boundary:
# Multi-tenant aware cron task
from psdi.server import MXServer
mxServer = MXServer.getMXServer()
userInfo = mxServer.getSystemUserInfo()
# Iterate over all tenants
tenantSet = mxServer.getMboSet("TENANT", userInfo)
tenant = tenantSet.getMbo(0)
while tenant is not None:
tenantCode = tenant.getString("TENANTCODE")
tenantUserInfo = mxServer.getUserInfoForTenant(tenantCode)
woSet = mxServer.getMboSet("WORKORDER", tenantUserInfo)
woSet.setWhere("STATUS NOT IN ('CLOSE', 'CAN')")
woSet.reset()
# ... tenant-scoped processing ...
woSet.close()
tenant = tenantSet.getMbo(1)
tenantSet.close()Without tenant scoping, a multi-tenant cron task operates on all tenants at once and may violate isolation. The MAS 9.1 getUserInfoForTenant API is the right entry point.
Common Pitfalls in MAS 9.1
The BIRT pitfalls are predictable. BIRT 4.9 deprecates Total.isnull(), changes how date math works in JavaScript expressions, and tightens security around Runtime.getResource. Reports that worked in MAS 8 fail in MAS 9.1 with BirtException: Cannot load JDBC driver. The fix is to update the JDBC driver registration to use the OSGi bundle activator pattern that MAS 9.1 requires.
The cron task pitfalls are mostly around Java 17 reflection. Custom Java cron tasks that use setAccessible(true) on private fields fail with InaccessibleObjectException. The fix is to use the public API of the MboSet or to wrap the reflection in a --add-opens JVM argument in the JVM options.
A third pitfall is treating cron tasks as transactional. A cron task that processes 10,000 records in one giant transaction holds a database lock the entire time. The pattern is to process records in batches of 200, save each batch, and continue. If the cron task fails midway, the partial work is preserved.
Best Practices
The best practices that emerge from three MAS 9.1 rollouts:
- Use Object Structures, not direct SQL, in BIRT. Direct SQL bypasses Maximo security and audit trails. Object Structures honor both.
- Wrap cron tasks in try/except and write to CRONLOG. Silent cron task failures are the most expensive operational issue in Maximo. The CRONLOG is your audit trail.
- Test BIRT reports in MAS 9.1 before the upgrade. Every report in the legacy environment should be exercised in the MAS 9.1 sandbox at least once. Reports that have not been run in years are likely to fail.
- Cache slow BIRT reports. Operational dashboards with hourly cadence should use the BIRT cache. The cache reduces load on Db2 and improves report queue throughput.
- Use multi-tenant safe patterns. Even in single-tenant deployments, the multi-tenant patterns keep the code portable and avoid the legacy assumption that there is only one organization in the instance.
Practical Implications
BIRT and cron tasks are the unglamorous backbone of Maximo Manage. They run the daily operations, produce the reports that drive decisions, and quietly update records behind the scenes. The MAS 9.1 upgrade is the right time to clean up the library: retire reports no one runs, replace direct SQL with Object Structures, add error handling to every cron task, and instrument with CRONLOG. The investment pays back in operational stability, audit readiness, and lower support costs.
For teams that deferred their MAS 9.1 upgrade past September 30, 2026, the Maximo 7.6 Sustained Support window closes with the IBM Maximo Application Suite end-of-support roadmap. The migration to MAS 9.1 is unavoidable, and the BIRT/cron task library is one of the largest bodies of code that needs review. Plan a 6-week review cycle for the report library and a 4-week review cycle for the cron task library. The first run is the most expensive; subsequent upgrades are cheaper.
Bottom Line
BIRT and cron tasks in Maximo Manage 9.1 work the same way they always have, but Java 17, BIRT 4.9, and MAS 9.1's tighter security model mean that every report and every cron task should be re-tested. The patterns above are the field-tested defaults that survive the upgrade cleanly. Adopt them and the report queue stays calm, the cron tasks stay auditable, and the operations team stays productive.
The deeper discipline that the MAS 9.1 upgrade forces is the audit of the entire report and cron task library. Most teams discover during the audit that 30 to 40% of the reports in the library are no longer used, 20% of the cron tasks have no error handling, and a handful of cron tasks have grown into shadow batch jobs that should be replaced by proper integration patterns. The audit is the value. The MAS 9.1 upgrade is the forcing function.
For teams that have not yet upgraded from Maximo 7.6 to MAS 9.1, the BIRT and cron task library is one of the largest bodies of code that needs review. The Java 8 to Java 17 transition will surface every reflection-heavy pattern, every deprecated JDBC driver reference, and every direct SQL data set. Plan a 6-week review cycle for the report library and a 4-week review cycle for the cron task library. Run the first pass in a non-production environment. The second pass should validate the fixes. The third pass is the production cutover.
For teams already on MAS 9.1, the right next step is to establish a continuous audit cadence. A quarterly review of the BIRT library (which reports are still in use, which reports need updates, which reports should be retired) keeps the library healthy. A quarterly review of the cron task library (which tasks are still needed, which tasks need error handling updates, which tasks should be replaced by integration patterns) keeps the cron tasks auditable. The cadence is sustainable and the operational stability is the return.
Sources
- [What's new in Maximo Manage 9.1](https://www.ibm.com/docs/en/masv-and-l/maximo-manage/cd?topic=manage-whats-new-in-maximo-91)
- [A collection of Automation Scripts (IBM Support)](https://www.ibm.com/support/pages/collection-automation-scripts)
- [Updating Work Order Dates in Maximo using Automation Script](https://www.youtube.com/watch?v=Lmg4niaGFjo)
- [Maximo Application Suite Releases information](https://www.ibm.com/support/pages/maximo-application-suite-releases-information-0)
- [Integration framework overview - Maximo Manage](https://www.ibm.com/docs/en/masv-and-l/maximo-manage/cd?topic=applications-integration-framework-overview)