All articles

Filtering Lookups That Actually Work

A practical guide to filtering lookup fields in model-driven apps using addPreSearch, addCustomFilter, and the no-code Related Records Filtering option.

· 6 min read

Lookup fields in model-driven apps show every record in the target table by default. That’s almost never what you want. A sales rep picking a contact shouldn’t see 50,000 contacts from every account in the system — they should see the contacts that belong to the account on the current form.

This article covers how to filter lookups using JavaScript and how to do it without code using the form designer.

The Core Pattern: addPreSearch + addCustomFilter

Filtering a lookup in a model-driven app always follows the same two-step pattern:

  1. Register a pre-search handler on the lookup field using addPreSearch
  2. Inside that handler, build a FetchXML filter and apply it with addCustomFilter

That’s it. Every lookup filter you’ll ever write uses this structure.

Here’s the skeleton:

function onLoad(executionContext) {
    var formContext = executionContext.getFormContext();
    formContext.getControl("primarycontactid").addPreSearch(filterContacts);
}

function filterContacts(executionContext) {
    var formContext = executionContext.getFormContext();

    var filter = `
        <filter type="and">
            <condition attribute="statuscode" operator="eq" value="1" />
        </filter>`;

    formContext.getControl("primarycontactid").addCustomFilter(filter, "contact");
}

The addPreSearch method registers a function that runs every time the user clicks the lookup or starts typing in it. Inside that function, addCustomFilter injects a FetchXML <filter> node into the lookup’s query. The second parameter of addCustomFilter is the entity logical name — you need it when the lookup can point to multiple entity types (polymorphic lookups like Customer or Regarding).

The XML Filter Format

The filter you pass to addCustomFilter is a FetchXML <filter> element. It supports the same operators you’d use in any FetchXML query:

<!-- Single condition -->
<filter type="and">
    <condition attribute="address1_city" operator="eq" value="Seattle" />
</filter>

<!-- Multiple conditions -->
<filter type="and">
    <condition attribute="address1_city" operator="eq" value="Seattle" />
    <condition attribute="statecode" operator="eq" value="0" />
</filter>

<!-- OR logic -->
<filter type="or">
    <condition attribute="address1_city" operator="eq" value="Seattle" />
    <condition attribute="address1_city" operator="eq" value="Portland" />
</filter>

<!-- IN operator -->
<filter type="and">
    <condition attribute="address1_city" operator="in">
        <value>Seattle</value>
        <value>Portland</value>
        <value>Redmond</value>
    </condition>
</filter>

Common operators: eq, ne, gt, lt, ge, le, like, in, not-in, null, not-null, under (for hierarchy), eq-businessid (current business unit).

Example 1: Filter Accounts by City

Show only accounts located in the same city as the current record.

function onLoad(executionContext) {
    var formContext = executionContext.getFormContext();
    formContext.getControl("parentaccountid").addPreSearch(filterAccountsByCity);
}

function filterAccountsByCity(executionContext) {
    var formContext = executionContext.getFormContext();
    var city = formContext.getAttribute("address1_city").getValue();

    if (!city) return; // no city set — don't filter

    var filter = `
        <filter type="and">
            <condition attribute="address1_city" operator="eq" value="${city}" />
        </filter>`;

    formContext.getControl("parentaccountid").addCustomFilter(filter, "account");
}

Every time the user opens the Parent Account lookup, this function reads the city from the current form and restricts results to accounts in that city. If the city field is empty, it skips the filter and shows all accounts.

Example 2: Filter Contacts by Parent Account

This is the most common lookup filtering scenario. You have a form with an Account lookup and a Contact lookup, and you want the Contact lookup to only show contacts that belong to the selected account.

function onLoad(executionContext) {
    var formContext = executionContext.getFormContext();
    formContext.getControl("primarycontactid").addPreSearch(filterContactsByAccount);
}

function filterContactsByAccount(executionContext) {
    var formContext = executionContext.getFormContext();
    var account = formContext.getAttribute("parentaccountid").getValue();

    if (!account) return;

    var accountId = account[0].id.replace("{", "").replace("}", "");

    var filter = `
        <filter type="and">
            <condition attribute="parentcustomerid" operator="eq" value="${accountId}" />
        </filter>`;

    formContext.getControl("primarycontactid").addCustomFilter(filter, "contact");
}

Notice the account[0].id pattern — lookup values are arrays. The ID comes back with curly braces, so we strip them before putting the value into FetchXML.

Static vs Dynamic Filters

Static filter — the filter criteria don’t change based on form data. Example: only show active records.

function filterActiveAccountsOnly(executionContext) {
    var formContext = executionContext.getFormContext();
    var filter = `
        <filter type="and">
            <condition attribute="statecode" operator="eq" value="0" />
        </filter>`;
    formContext.getControl("parentaccountid").addCustomFilter(filter, "account");
}

This filter is the same every time. You could register it once on load and forget about it.

Dynamic filter — the filter reads values from the form at the moment the lookup opens. The city and parent account examples above are dynamic. The pre-search handler runs each time, reads the current form values, and builds a fresh filter.

Dynamic filters just work because addPreSearch fires every time the user interacts with the lookup. You don’t need to remove and re-add anything when form values change.

Removing Filters

If you need to stop filtering a lookup (for example, based on a checkbox the user toggles), use removePreSearch:

function onLoad(executionContext) {
    var formContext = executionContext.getFormContext();
    formContext.getControl("primarycontactid").addPreSearch(filterContactsByAccount);

    // listen for changes on a toggle field
    formContext.getAttribute("cr123_showallcontacts").addOnChange(toggleContactFilter);
}

function toggleContactFilter(executionContext) {
    var formContext = executionContext.getFormContext();
    var showAll = formContext.getAttribute("cr123_showallcontacts").getValue();

    if (showAll) {
        formContext.getControl("primarycontactid").removePreSearch(filterContactsByAccount);
    } else {
        formContext.getControl("primarycontactid").addPreSearch(filterContactsByAccount);
    }
}

The function reference you pass to removePreSearch must be the same function you registered with addPreSearch. This is why you should use named functions, not anonymous ones.

If your filter is “show contacts that belong to the selected account” and it follows a direct relationship, you don’t need JavaScript at all.

The model-driven form designer has a built-in Related Records Filtering feature:

  1. Open the form in the form designer
  2. Select the lookup field you want to filter
  3. In the properties panel, expand Filtering
  4. Check Related Records Filtering (the exact label may say “Only show records where”)
  5. Select the relationship — for example, “Contact → Parent Customer (Account)”
  6. Choose which field on the current form provides the filter value

This sets up the same kind of filter we wrote in Example 2, but with zero code. It works well for straightforward relationship-based filtering.

When to use it vs JavaScript:

ScenarioApproach
Filter by a direct relationship on the formRelated Records Filtering (no code)
Filter by a field value (city, status, type)JavaScript
Conditional filtering (only filter when a checkbox is set)JavaScript
Filter involving complex logic or multiple conditionsJavaScript
Filter based on the current user’s team or business unitJavaScript

The no-code option covers maybe 30% of real-world cases. For everything else, you need the addPreSearch/addCustomFilter pattern.

Putting It Together: A Real Registration Example

Here’s how I typically structure lookup filtering in a production web resource. One onLoad function, multiple filters, all registered in one place.

var Contoso = Contoso || {};
Contoso.CaseForm = (function () {
    "use strict";

    function onLoad(executionContext) {
        var formContext = executionContext.getFormContext();

        // Filter primary contact by the selected customer account
        formContext.getControl("primarycontactid").addPreSearch(filterContactsByCustomer);

        // Only show active products in the product lookup
        formContext.getControl("productid").addPreSearch(filterActiveProducts);
    }

    function filterContactsByCustomer(executionContext) {
        var formContext = executionContext.getFormContext();
        var customer = formContext.getAttribute("customerid").getValue();

        if (!customer || customer[0].entityType !== "account") return;

        var accountId = customer[0].id.replace("{", "").replace("}", "");

        var filter = `
            <filter type="and">
                <condition attribute="parentcustomerid" operator="eq" value="${accountId}" />
            </filter>`;

        formContext.getControl("primarycontactid").addCustomFilter(filter, "contact");
    }

    function filterActiveProducts(executionContext) {
        var formContext = executionContext.getFormContext();
        var filter = `
            <filter type="and">
                <condition attribute="statecode" operator="eq" value="0" />
            </filter>`;
        formContext.getControl("productid").addCustomFilter(filter, "product");
    }

    return {
        onLoad: onLoad
    };
})();

Register Contoso.CaseForm.onLoad as the form’s OnLoad handler. Each filter is a separate named function, easy to test and easy to remove later.

Quick Reference

MethodPurpose
control.addPreSearch(handler)Register a function that runs before every lookup search
control.addCustomFilter(fetchXmlFilter, entityType)Inject a FetchXML filter into the lookup query
control.removePreSearch(handler)Remove a previously registered pre-search handler

Tip: When debugging lookup filters, open the browser dev tools (F12), add a debugger; statement inside your pre-search handler, then click the lookup. You’ll hit the breakpoint and can inspect the filter string before it gets applied. This saves a lot of guessing when your filter isn’t returning the results you expect.

Share this article LinkedIn X / Twitter

Related articles

Subgrids Done Right

Subgrids show related records directly on a form. Here's how to add them, choose the right relationship, filter the records, enable inline editing, and set up a New button that pre-fills the parent lookup.

· 7 min read