Agile PX: Demoting a Change Order

Demoting a change with an obsolete part on the BOM of an affected item

I often go through the Agile PLM SDK group in LinkedIn.  On a recent visit, someone was having an issue with obsolete parts showing on the BOM of an item on a change order.  To be clear, the issue is not that parts on the change were obsolete, it is that parts on the BOM of the affected items of a change were obsolete.  A process extension (PX) was proposed by another group member to remedy this business rule.  This blog illustrates the implementation of that process extension.

 Agile PLM Change Order containing part 140002 (not obsolete) and its BOM.

Agile Change Order

Here is the title page of the item (140001) that was on the BOM of the affected item (140002)

Obsolete Title Block

Here is the functional decomposition of what needs to happen in the Agile PLM process extension (PX) to get the desired results:

  1. Pull the affected items table for the change order passed into the process extension
    1. For each item on the change
      1. Get the underlying Agile item from the referenced affected item
        1. Get the BOM of the affected item
          1. Look at the title page lifecycle attribute for each item on the BOM
      2. If we find an obsolete part, we need to demote
  2. If we need to demote
    1. Get the workflow status’
    2. Make sure we are not at the beginning of the workflow – if we are, do nothing
    3. Demote to the prior workflow state and add a history item

Here is the aftermath the process extension fires

PX Results

And now for the source code:

package com.vestalsgap.px;
import com.agile.api.*;
import com.agile.px.ActionResult;
import java.util.*;
import org.apache.log4j.Logger;
public class EcoDemoter extends AbstractPX  {
     * Process Extension Functionality
     * Configure a Process Extension which can run on workflow change status to get Items on
     * Change Order and check all of its BOM for Obselete part if its failing, Dont release the
     * Change Order.
    // consider a history message that mentions the actual affected items that are in violation
    // and a format string read from configuration rather than hard coded - this is only an example
    String history = "The change order was demoted because the BOM of one or more affected items contained an obsolete item";
    // again this should be a configuration setting
    String invalidLifecycles = ",Obsolete,";
    public ActionResult doAction(IAgileSession session, INode actionNode, IDataObject affectedObject) {
        // only process change orders
        ActionResult outcome = new ActionResult();
        if ( !(affectedObject instanceof IChange) ) return outcome;        
        IChange change = (IChange) affectedObject;        
        try {
            ITable affectedItems = change.getTable(ChangeConstants.TABLE_AFFECTEDITEMS);
            Iterator<IRow> rows = affectedItems.iterator();
            IRow row;
            IItem affectedItem = null;
            while(rows.hasNext()) {
                // get the affected item
                row = (IRow) (;
                affectedItem = (IItem) row.getReferent();
                if ( !hasValidBOM(affectedItem) ) {
                    outcome = new ActionResult(ActionResult.STRING, history);
                    demote(change, history);
            return outcome;
        } catch (Throwable th) {
            logger.error("Error: " + th.getMessage());
            return new ActionResult(ActionResult.EXCEPTION, th);
    private void demote(IChange change, String reason) throws Exception {
        IStatus[] states = change.getWorkflow().getStates();
        IStatus status = change.getStatus();
        // a change cannot be demoted if it is already at the beginning of the workflow
        if (states[0] == status) {
   + " is already at the first workflow step ... cannot demote.");
        for (int i=0; i<states.length; i++) {
            if (states[i] == status && i>0) {
                status = states[i-1];
        Object[]  approvers = change.getApproversEx(status);  
        Object[]  observers = change.getObserversEx(status);
        change.changeStatus(status, false, reason, true, false, new IUser[0], approvers, observers, false);
    private boolean hasValidBOM(IItem parent) throws Exception {
        ITable items = parent.getTable(ItemConstants.TABLE_BOM);
        Iterator<IRow> rows = items.iterator();
        IRow row;
        String lifecycle = "";
        IItem item;
        while(rows.hasNext()) {
            row =;
            // This should be a configuration parameter to check against a list of phases
            lifecycle = row.getReferent().getValue(ItemConstants.ATT_TITLE_BLOCK_LIFECYCLE_PHASE).toString();
            if (lifecycle != null && invalidLifecycles.indexOf(lifecycle) > -1) return false;
        return true;