Plugin Best Practices

Introduction

Great Encompass plugins share a few key features that set them apart from the rest. They are performant, reliable, do what they advertise to perfection, and blend in naturally with Encompass features.

These development guidelines were developed by our engineering team at ICE Mortgage Technology (IMT) through collaboration with our different teams and partners, and through observations/research of many plugins in our customer environments. Many lessons were learned through this process on how greatly plugins can affect the stability of Encompass and its performance.

With these lessons and research data captured, we consolidated and evolved this set of best practices. Like with any guidelines, there are always legitimate exceptions and differing approaches that are equally valid. Please consider incorporating these guidelines into your development standards when implementing new plugins, or just maintaining existing ones.

Ensure if a Plugin is Necessary

There are several different approaches to introduce your logic and extend functionality in the Encompass lending platform. Ensure that a custom plugin is actually necessary. For example, you can avoid creating plugins for customizations that can be achieved using built-in means such as business rules for field triggers, custom fields, field data entry rules, persona checks, and/or workflow rules.

One Plugin to Rule Them All

If possible, combine multiple plugin functionality into a single JavaScript file; as this makes it more efficient to load when Encompass starts up. Large number of plugins can affect startup times in Encompass and can impact network performance.

Use Javascript Bundlers During Development

Javascript bundlers like Rollup or Webpack can help you manage your plugin's dependencies, optimize your code, and streamline your development process. By using a bundler, you can take advantage of modern JavaScript features, modularize your code, and ensure compatibility across different environments. Bundlers help you:

  • Manage dependencies: Easily include and manage third-party libraries and modules in your plugin.
  • Optimize code: Minify and compress your code to reduce file size and improve load times.
  • Transpile code: Convert modern JavaScript (ES6+) into a format compatible with browsers.
  • Modularize code: Break your code into smaller, reusable modules for better organization and maintainability.
  • Automate tasks: Set up automated build processes to streamline development and deployment.
  • Tree shaking: Remove unused code to further reduce bundle size.
  • Plugin packaging: Bundle your plugin into a single file for easy deployment to Encompass.
  • Linting and formatting: Enforce coding standards and style consistency across your codebase.

Be Secure - Take Care with your Sensitive/PII Data

Do not cache or persist any data that contains or may contain any PII (Personal Identifiable Information) or any access credentials. This may include: Loan Data, LoanCustomData, Pipeline Data. API Keys, Access Tokens, User Credentials, etc.

Do not log PII, loan data, credentials, or any sensitive information to the console, even during development. Implement conditional logging based on environment.

Log Activities to Aid in Debugging

If possible, log important events and any significant relevant data using console. This aids in troubleshooting any error that may arise when running your plugin.

Look Out for Performance Bottlenecks

Care must be taken to avoid recursive calls when updating the loan or loan fields in response to the events listed below:

  • loan.Change
  • loan.Committed
  • loan.PreCommit
  • loan.Sync

If you are setting field data and executing loan calculations, consider using the following steps:

  1. Set all fields in 1 go => loan.setFields({field1:val1, field2:val2, etc...})
  2. Execute the calculation once all fields are set: loan.calculate()

Why does this matter?

  • Every loan.setFields call raises a loan.change event. By doing a single loan.setFields , you'll get a single loan.change event to action in deeper scripts, resulting in a lowered chance of potential recursive flows.
  • Every loan.calculate() executes an async API call, which costs time. This is equal to network processing time + calculation engine time, which is dependent upon the number of rules you have.
  • Every loan API call that changes the loan, results in a loan.sync event. Similar to reduced loan.change events, if we have reduce loan.sync events, then we reduce the potential for recursive flows.

Keep Event Handler Subscriptions in a Single File

Care must be taken to keep all event handler subscriptions in a single file, such as during plugin initialization. This helps to avoid multiple subscriptions to the same event, which can lead to unexpected behaviors and hard-to-trace bugs.

Plugins are UI-less

Plugins cannot have any user interface (UI). They are purely logic-based and run in the background without any visual components. Do not attempt to display alerts, modify the DOM, or render any UI elements.

Process Sync Events Quickly

Sync events can be triggered frequently. Ensure that your sync event handlers are optimized for performance and do not perform long-running operations.

Avoid Relying on Global State

Avoid using global variables, web storage (session / local storage) to store state information. Instead, encapsulate state within functions or classes to prevent unintended side effects and improve code maintainability.

CORS Errors

Be aware of Cross-Origin Resource Sharing (CORS) errors when making API calls from your plugin. Ensure that your API endpoints are configured to allow requests from the Encompass origins using appropriate CORS headers.

Encompass Developer Connect (EDC) API Usage

When using EDC APIs, ensure that you follow best practices for authentication, error handling, and rate limiting. Always refer to the latest EDC API documentation for guidance on proper usage.

Authentication for EDC API Calls

When making EDC API calls, use the auth.getAccessToken() method to obtain a valid access token. This ensures that your plugin adheres to Encompass security protocols and maintains proper authentication. Legacy auth.createAuthCode()method is deprecated and should be avoided.

Code Samples


Cache Scripting Objects to Minimize API Calls

Each call to elli.script.getObject() or scripting object methods involves cross-frame communication overhead. Cache object references when possible and reuse them throughout your plugin's lifecycle.

// BAD: Repeatedly requesting the same object
async function updateMultipleFields() {
  const loan1 = await elli.script.getObject("loan");
  await loan1.setFields({ 1109: 400000 });

  const loan2 = await elli.script.getObject("loan"); // Unnecessary duplicate call
  await loan2.setFields({ 4001: "John" }); 
}

// GOOD: Cache and reuse the object reference
let cachedLoan = null;

async function getLoan() {
  if (!cachedLoan) {
    cachedLoan = await elli.script.getObject("loan");
  }
  return cachedLoan;
}

async function updateMultipleFields() {
  const loan = await getLoan();
  await loan.setFields({ 1109: 400000, 4001: "John" });
}

📘

Important

Invalidate your cache when the context changes (e.g., when a different loan is opened).

Always Handle Errors and Validate Objects

Scripting objects may not always be available depending on the context. Always wrap API calls in try-catch blocks and validate object availability before use.

// BAD: No error handling
async function updateLoan() {
  const loan = await elli.script.getObject("loan");
  await loan.setFields({ 1109: 400000 }); // Will crash if loan is not available
}

// GOOD: Defensive programming with error handling
async function updateLoan() {
  try {
    const loan = await elli.script.getObject("loan");
    await loan.setFields({ 1109: 400000 });
  } catch (error) {
    console.error("Failed to update loan:", error);
    // Implement fallback behavior or user notification
  }
}

Batch Field Operations

Avoid making individual API calls for each field operation. Batch read and write operations to reduce cross-frame communication overhead and improve performance.

// BAD: Multiple individual field operations
async function updateFields() {
  const loan = await elli.script.getObject("loan");
  await loan.setFields({ 1109: 400000 });
  await loan.setFields({ 4001: "John" });
  await loan.setFields({ 4002: "Doe" });
}

// GOOD: Batch all field operations in a single call
async function updateFields() {
  const loan = await elli.script.getObject("loan");
  await loan.setFields({
    1109: 400000,
    4001: "John",
    4002: "Doe",
  });
}

Use Async/Await Consistently

All scripting object interactions are asynchronous. Use async/await patterns consistently to avoid callback problems and to make your code more readable and maintainable.

// BAD: Mixing callbacks and promises
function updateLoan() {
  elli.script.getObject("loan").then((loan) => {
    loan.getField("4001").then((name) => {
      loan.setFields({ 4002: name }).then(() => {
        console.log("Done");
      });
    });
  });
}

// GOOD: Clean async/await pattern
async function updateLoan() {
  try {
    const loan = await elli.script.getObject("loan");
    const name = await loan.getField("4001");
    await loan.setFields({ 4002: name });
    console.log("Done");
  } catch (error) {
    console.error("Error:", error);
  }
}

Avoid Memory Leaks

Clear intervals and timeouts, and nullify large object references when they're no longer needed. Even in a sandboxed environment, memory leaks can degrade performance over time.

// BAD: Intervals and references not cleaned up
let timer = setInterval(checkLoanStatus, 5000);
let largeDataCache = [];

// GOOD: Proper cleanup
let timer = null;
let largeDataCache = [];

function startMonitoring() {
  timer = setInterval(checkLoanStatus, 5000);
}

function stopMonitoring() {
  if (timer) {
    clearInterval(timer);
    timer = null;
  }
  largeDataCache = null;
}

Avoid Recursive Situations

Executing calculations inside a loan change event handler can cause unwanted recursion. When calculations modify loan fields, those changes trigger the same event handler, which in turn runs the calculations again. This can create an unwanted cycle of repeated updates.

// Bad Practice - triggering calculation on every change event
async function handleChangeEvent() {
  const loanObj = await elli.script.getObject("loan");
  await loanObj.calculate();
}

elli.script.subscribe("loan", "change", handleChangeEvent);

Preventing Loan Context Errors in Async Operations

When performing long-running async operations (e.g., setTimeout, fetch, API calls) in loan event handlers, the user might switch to a different loan before the operation completes. This causes field updates on the wrong loan.

Unsafe

❗️

The following is an example of unsafe code:

elli.script.subscribe("loan", "change", async (obj, data) => {
  // Any async operation - setTimeout, fetch, etc.
  const result = await someAsyncOperation();
  await loanObj?.setFields?.({ 66: result }); // May end up in wrong loan!
});

The Solution - Three Simple Steps

  1. Capture loan ID before the async operation.
  2. Check if loan context is the same after the operation completes and before making changes to the loan.
  3. Only perform loan update if context hasn't changed.
let loanObj = null;

const getLoanId = async () => {
  const loanData = await loanObj?.all?.();
  return loanData?.loan?.id;
};

const isLoanContextSame = async (prevLoanId) => {
  const curLoanId = await getLoanId();
  return prevLoanId === curLoanId;
};

const updateHomePhone = async (data) => {
  const prevLoanId = await getLoanId(); // Step 1: Capture loan ID

  setTimeout(async () => {
    if (data.fieldID === "4000") {
      if (await isLoanContextSame(prevLoanId)) {
        // Step 2: Validate
        await loanObj?.setFields?.({ 66: "8888622100" }); // Step 3: Execute
        console.log("home phone modified");
      } else {
        console.error("Loan context changed, no action taken");
      }
    }
  }, 60000);
};

elli.script.subscribe("loan", "open", async (obj, data) => {
  loanObj = await elli.script.getObject("loan");
});

elli.script.subscribe("loan", "change", async (obj, data) => {
  await updateHomePhone(data);
});
// refer to Example 1 for getLoanId and isLoanContextSame functions

const fetchAndUpdateCreditScore = async (data) => {
  const prevLoanId = await getLoanId(); // Step 1: Capture loan ID

  if (data.fieldID === "1234") {
    try {
      // Long-running API call
      const response = await fetch("https://api.example.com/credit-score");
      const creditData = await response.json();

      // Step 2: Validate after async operation
      if (await isLoanContextSame(prevLoanId)) {
        await loanObj?.setFields?.({ creditScore: creditData.score }); // Step 3: Execute
        console.log("Credit score updated");
      } else {
        console.error("Loan context changed during API call");
      }
    } catch (error) {
      console.error("Failed to fetch credit score:", error);
    }
  }
};

elli.script.subscribe("loan", "change", async (obj, data) => {
  await fetchAndUpdateCreditScore(data);
});

Test in Isolated Environment First

Before uploading to production:

  • Test your plugin in a development environment.
  • Verify all scripting object interactions work correctly.
  • Test error handling by simulating failure scenarios.
  • Check performance with large data sets.
  • Verify cleanup and memory management

Don't Test Using Admin

When testing your plugin, make sure to use a proper test account with similar rights that you would expect an end user to have. Some features and checks are disabled while running under an admin account that you might not be exposed to until your code is released. Therefore, it's always a good idea to test your code with a lower account first. Also, if a mistake is made, the resulting bad experience is a bit more contained.

Monitor Console for Errors

During development, actively monitor the browser console (Encompass Web) / Trace Viewer (Encompass Desktop) for the following:

  • Unhandled promise rejections
  • API call failures
  • Performance warnings
  • Security violations

Test Cross-Browser Compatibility

Since plugins run in iframes, ensure compatibility across supported browsers:

  • Chrome/Edge (Chromium)
  • Firefox
  • Safari

Test Web and Desktop Compatibility

Ensure your plugin works seamlessly in both Encompass Web and Encompass Desktop environments; as there may be subtle differences in behavior.