DEV Community

Explorer
Explorer

Posted on

🔀 Implement Item-Level Task Delegation in Joget with BeanShell

1. Overview

This article shows a Joget BeanShell pattern for item-level task delegation. Instead of reassigning an entire process blindly, the script reads selected delegation rows, updates workflow variables, reassigns the active activity, and writes an audit trail.

2. How It Works

  • Read selected delegation rows from a grid or hidden JSON field.
  • Resolve the current assignee, target user, activity ID, and parent record ID.
  • Update routing variables for the selected activity.
  • Reassign the activity with workflowManager.assignmentReassign(...).
  • Remove extra assignees when the activity currently has multiple users.
  • Insert an audit row so reassignment history is traceable.

3. Where to Use in Joget

  • Workflow Builder: Tool or store binder logic for reassignment forms.
  • Form Builder: item-level delegation screen with a grid of selected activities.
  • Userview: admin-only reassignment pages.

4. Full Code

import org.joget.apps.form.model.FormRowSet;
import org.joget.apps.form.model.FormRow;
import org.joget.apps.app.service.AppUtil;
import java.sql.Connection;
import java.sql.Statement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import javax.sql.DataSource;
import org.joget.commons.util.LogUtil;
import org.joget.workflow.model.service.WorkflowManager;
import org.joget.workflow.model.WorkflowAssignment;
import org.joget.workflow.model.service.WorkflowUserManager;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.Collection;
import java.util.ArrayList;




public void insertAuditTrail(
        String recordId,
        String processName,
        String activityName,
        String assignedUsers,
        String activityId) {

    Connection con = null;
    PreparedStatement stmt = null;

    try {
        DataSource ds = (DataSource) AppUtil.getApplicationContext().getBean("setupDataSource");
        con = ds.getConnection();

        if (!con.isClosed()) {

            String id = java.util.UUID.randomUUID().toString();
            String now = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
                    .format(new java.util.Date());

            // String sql =
            //     "INSERT INTO app_fd_assignment_audit " +
            //     "(id, c_generic_field, c_generic_field, c_generic_field, dateCreated, c_generic_field) " +
            //     "VALUES (?, ?, ?, ?, ?, ?)";

            String sql =
                "INSERT INTO app_fd_assignment_audit " +
                "(id, c_generic_field, c_generic_field, c_generic_field, " +
                "dateCreated, c_generic_field, " +
                "c_generic_field, c_generic_field, c_generic_field, c_generic_field) " +
                "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";

            stmt = con.prepareStatement(sql);

            FormRow row = rows.get(0);
            String doneByUsername = row.get("requesting_user");
            String doneByFullName = row.get("requesting_user_name");
            String notes = row.get("notes");


            String actionDetails  = "Reassigned";

            int i = 1;
            stmt.setString(i++, id);
            stmt.setString(i++, recordId);
            stmt.setString(i++, processName);
            stmt.setString(i++, activityName);
            stmt.setString(i++, now);
            stmt.setString(i++, assignedUsers);
            stmt.setString(i++, actionDetails);      // Reassigned
            stmt.setString(i++, doneByUsername);     // Done by (username)
            stmt.setString(i++, doneByFullName);     // Done by (full name)
            stmt.setString(i++, notes);  


            stmt.executeUpdate();

            WorkflowManager wm =
                (WorkflowManager) AppUtil.getApplicationContext().getBean("workflowManager");

            wm.activityVariable(activityId, "history_uuid", id);
        }

    } catch (Exception e) {
        LogUtil.error("Bulk Reassign Audit", e, "Error inserting audit trail");
    } finally {
        try { if (stmt != null) stmt.close(); } catch (Exception e) {}
        try { if (con != null) con.close(); } catch (Exception e) {}
    }
}

public String getActivityName(String activityId) {
    try {
        WorkflowManager wm =
            (WorkflowManager) AppUtil.getApplicationContext().getBean("workflowManager");

        return wm.getActivityById(activityId).getName();
    } catch (Exception e) {
        LogUtil.error("Bulk Reassign Audit", e, "Unable to fetch activity name");
        return activityId;
    }
}



public void removeAssignee(String activityId, String username) {
   Connection con = null;
   Statement stmt = null;

   try {
      DataSource ds = (DataSource) AppUtil.getApplicationContext().getBean("setupDataSource");
      con = ds.getConnection();

      if (!con.isClosed()) {
         LogUtil.info("ExecuteSQL", "Connection to database established successfully.");
         stmt = con.createStatement();

         stmt.executeUpdate("DELETE FROM SHKAssignmentsTable where ActivityId = '" + activityId + "' and ResourceId = '" + username + "'");

         LogUtil.info("Remove Assignee", "SQL statements executed successfully.");
      } else {
         LogUtil.warn("Remove Assignee", "Connection to database failed.");
      }
   } catch (Exception e) {
      LogUtil.error("Remove Assignee", e, "Error executing SQL statements");
   } finally {
      try {
         if (stmt != null) {
            stmt.close();
         }
         if (con != null && !con.isClosed()) {
            con.close();
         }
      } catch (Exception e) {
         LogUtil.error("Remove Assignee", e, "Error closing DB connection");
      }
   }
}

public FormRowSet storeData() {
   WorkflowManager wm = (WorkflowManager) AppUtil.getApplicationContext().getBean("workflowManager");
   WorkflowUserManager wum = (WorkflowUserManager) AppUtil.getApplicationContext().getBean("workflowUserManager");
   FormRow row = rows.get(0);

   String branchCode = row.get("routing_group_code");
   String selectedUser = row.get("target_user");
   String selectedTargetUnit = row.get("target_unit_id");

   try {
      String allReassignJsonArray = row.get("delegation_rows");
      //LogUtil.info("Multiple Reassign", "allReassignJson: " + allReassignJsonArray);

      if (allReassignJsonArray == null || allReassignJsonArray.isEmpty()) {
         //LogUtil.info("Multiple Reassign", "No reassign rows found");
         return null;
      }

      JSONArray allReassignJson = new JSONArray(allReassignJsonArray);

      //LogUtil.info("Multiple Reassign", "allReassignJson: " + allReassignJson.length());

      for (int i = 0; i < allReassignJson.length(); i++) {
         JSONObject reassignJson = allReassignJson.getJSONObject(i);
         String currentAssignee = reassignJson.getString("current_assignee");
         String activityId = reassignJson.getString("activity_id");
         String recordId = reassignJson.getString("record_id");


         // ************************ code for historical data *****************************************

         boolean isRequestProcess = activityId != null && activityId.contains("main_process_key");

         boolean isSecondary RequestProcess = activityId != null && activityId.contains("secondary_process_key");

         String processName = isRequestProcess ? "Main Process" : (isSecondary RequestProcess ? "Secondary Process" : null);

         // *******************************************************************************************

         String currentUser = wum.getCurrentThreadUser();
         wum.setCurrentThreadUser(currentAssignee);
         wum.setCurrentThreadUser(currentUser);



         // 2. Newly added for :: Main Process :: Muscat (Section or Department Changing)
         if (activityId != null && activityId.contains("main_process_key")) {
            wm.activityVariable(activityId, "target_section_code", selectedTargetUnit);
            wm.activityVariable(activityId, "target_unit_id", selectedTargetUnit);
            wm.activityVariable(activityId, "routing_group_code", branchCode);
         }

         //3. newly added for Complain Resercher :: Main Process
         if (activityId != null && activityId.contains("main_process_key") && (activityId.contains("activity_key_1") ||
               activityId.contains("activity_key_2") || activityId.contains("activity_key_3") ||
               activityId.contains("activity_key_4") || activityId.contains("activity_key_5") ||
               activityId.contains("activity_key_6") || activityId.contains("activity_key_7") ||
               activityId.contains("activity_key_8") || activityId.contains("activity_key_9"))) {
            wm.activityVariable(activityId, "assigned_handler", selectedUser);
         }

         // 2. Newly added for :: Secondary Process :: Muscat (Section or Department Changing)
         if (activityId != null && activityId.contains("secondary_process_key")) {
            wm.activityVariable(activityId, "request_type_code", selectedTargetUnit);
            wm.activityVariable(activityId, "routing_group_code", branchCode);
         }

         //newly added for Judicial Officer :: Secondary Process
         if (activityId != null && activityId.contains("secondary_process_key") && (activityId.contains("review_activity_1") ||
               activityId.contains("review_activity_2") || activityId.contains("remarks_after_review_activity_2") ||
               activityId.contains("review_activity_4") || activityId.contains("review_activity_5") ||
               activityId.contains("review_activity_6") || activityId.contains("review_activity_2_and_verification") ||
               activityId.contains("review_activity_8") || activityId.contains("review_activity_5_arrest_2") ||
               activityId.contains("review_activity_5_absent") || activityId.contains("review_activity_11"))) {
            Connection con = null;
            PreparedStatement ps = null;

            try {
               con = AppUtil.getApplicationContext().getBean("setupDataSource", javax.sql.DataSource.class).getConnection();

               if (!con.isClosed()) {

                  String updateQuery = "UPDATE app_fd_related_request SET c_generic_field=? WHERE id=?";

                  ps = con.prepareStatement(updateQuery);
                  ps.setString(1, selectedUser);
                  ps.setString(2, recordId);
                  ps.executeUpdate();
               }

            } catch (SQLException e) {
               LogUtil.info("Error updating inspector handler", e);

            } finally {
               try {
                  if (ps != null) ps.close();
               } catch (Exception e) {}

               try {
                  if (con != null) con.close();
               } catch (Exception e) {}
            }
         }

         if (currentAssignee.contains(",")) {
            String[] users = currentAssignee.split(", ");
            for (int i = 0; i < users.length; i++) {
               if (i == 0) {
                  wm.assignmentReassign("", "", activityId, selectedUser, users[i]);

                  // ** for historical data creation *************
                    if (processName != null) {
                         String activityName = getActivityName(activityId);
                        insertAuditTrail(
                            recordId,
                            processName,
                            activityName,     // activity name (same pattern you already use)
                            selectedUser,   // new current_assignee
                            activityId
                        );
                    }
                    // **********************************************


               } else {
                  removeAssignee(activityId, users[i]); // remove other current_assignees from the activity
               }
            }

         } else {
            wm.assignmentReassign("", "", activityId, selectedUser, currentAssignee);

            // ** for historical data creation *************
            if (processName != null) {

                String activityName = getActivityName(activityId);

                insertAuditTrail(
                    recordId,
                    processName,
                    activityName,     // activity name (same pattern you already use)
                    selectedUser,   // new current_assignee
                    activityId
                );
            }
            // **********************************************

         }
      }

   } catch (Exception e) {
      LogUtil.error("Multiple Reassign", e, "Error executing multiple reassign");
   }

   return rows;
}

return storeData();
Enter fullscreen mode Exit fullscreen mode

5. Example Use Cases

  • Delegating a specific task item to another user.
  • Bulk reassignment when an employee is unavailable.
  • Updating department or section routing for an active activity.
  • Maintaining a custom reassignment audit trail.

6. Customization Tips

  • Replace the audit table and field names with your own generic table.
  • Keep direct workflow table updates limited and well-tested.
  • Store reassignment reason or notes for audit requirements.
  • Restrict this action to admin or process-owner roles.

7. Key Benefits

  • Reassigns only the selected activity items.
  • Supports multiple current assignees.
  • Updates workflow variables during reassignment.
  • Keeps a traceable audit record.

8. Security Note

Task reassignment can change workflow ownership. Protect the form with permissions, avoid exposing internal process keys publicly, and log who performed the delegation.

9. Final Thoughts

This pattern is useful for operational teams that need controlled task delegation. Start with a test activity, verify the audit trail, and only then enable the action for production users.

Top comments (0)