" MicromOne

Pagine

Handling Asynchronous Logic in Dynamics 365 Using Promises and Async/Await

When building custom client-side scripts in Microsoft Dynamics 365 / Power Apps, you’ll often need to make asynchronous Web API calls — for example, to validate data before saving a record.

This guide explains how to handle asynchronous logic using both the classic Promise syntax (.then()) and the modern async/await approach, with clean, adaptable examples for your own entities.

Scenario: Validate Data Before Save

Suppose you need to check whether a record already exists before saving.
You can call a custom action or Web API endpoint to perform that validation and decide whether to continue with the save.

We’ll look at two approaches:

  1. The classic way using new Promise() and .then()

  2. The modern way using async/await

Classic Approach — Using new Promise() and .then()

CheckRecord: function (formContext, callback) {
    return new Promise((resolve, reject) => {
        // Example: read values from the form
        var fieldA = formContext.getAttribute("fieldA")?.getValue() || "";
        var fieldB = formContext.getAttribute("fieldB")?.getValue() || "";

        // Build the request for your custom action
        var request = {
            CustomAction_RecordData: {
                "@odata.type": "Microsoft.Dynamics.CRM.sampleentity",
                "fieldA": fieldA,
                "fieldB": fieldB
            },
            getMetadata: function () {
                return {
                    boundParameter: null,
                    parameterTypes: {
                        CustomAction_RecordData: { typeName: "mscrm.sampleentity", structuralProperty: 5 }
                    },
                    operationType: 0,
                    operationName: "new_CustomAction"
                };
            }
        };

        // Execute the Web API call
        Xrm.WebApi.execute(request).then(async response => {
            const contentType = response.headers.get("content-type");
            if (contentType && contentType.includes("application/json")) {
                const jsonResponse = await response.json();
                if (jsonResponse && callback) {
                    callback();
                    resolve(true); // duplicate found
                }
            } else {
                resolve(false); // no duplicate
            }
        }).catch(error => {
            console.error("Error in CheckRecord:", error);
            reject(error);
        });
    });
},

Why This Approach Is Verbose

  • You must manually create a new Promise().

  • You need to chain .then() and .catch().

  • Error handling and readability can become messy with nested logic.

Modern Approach — Using async and await

CheckRecord: async function (formContext, callback) {
    try {
        // Read values from the form
        const fieldA = formContext.getAttribute("fieldA")?.getValue() || "";
        const fieldB = formContext.getAttribute("fieldB")?.getValue() || "";

        // Build the request
        const request = {
            CustomAction_RecordData: {
                "@odata.type": "Microsoft.Dynamics.CRM.sampleentity",
                "fieldA": fieldA,
                "fieldB": fieldB
            },
            getMetadata: function () {
                return {
                    boundParameter: null,
                    parameterTypes: {
                        CustomAction_RecordData: { typeName: "mscrm.sampleentity", structuralProperty: 5 }
                    },
                    operationType: 0,
                    operationName: "new_CustomAction"
                };
            }
        };

        // Await the Web API response
        const response = await Xrm.WebApi.execute(request);
        const contentType = response.headers.get("content-type");

        if (contentType && contentType.includes("application/json")) {
            const jsonResponse = await response.json();
            if (jsonResponse && callback) {
                callback();
                return true; // duplicate found
            }
        }

        return false; // no duplicate found
    } catch (error) {
        console.error("Error in CheckRecord:", error);
        return null; // error case
    }
},

Comparison: Classic vs Modern

Promise creation Manual (new Promise()) Automatic (async handles it)
Error handling .catch() try...catch
Code style Nested and verbose Linear and clean
Readability Harder to follow Easy, top-to-bottom flow

Using It in the Save Event

Example with .then()

MyNamespace.CheckRecord(formContext, () => {
    // handle duplicate
}).then(result => {
    if (result === false) {
        formContext.data.save(); // proceed with save
    }
});

Example with await

const result = await MyNamespace.CheckRecord(formContext, callback);
if (result === false) {
    formContext.data.save();
}

When to Still Use new Promise()

You only need to use new Promise() when working with APIs that do not return a Promise — for example:

function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

In most Dynamics 365 Web API scenarios, Xrm.WebApi.execute() already returns a Promise, so using async/await is cleaner and safer.

Best Practices

  • Every Web API call in Dynamics 365 is asynchronous

  • Use async/await for simpler, cleaner syntax

  • Use new Promise() only with legacy or callback-based APIs

  • Keeping code linear and readable improves debugging, maintenance, and the user experience


Handling Form Save Events with Pre-Save Validation in Dynamics 365 Using JavaScript


When customizing forms in Microsoft Dynamics 365, it’s common to need validation logic before saving a record — for example, checking for duplicates or ensuring data consistency.

This article shows how to manage the OnSave event and perform a custom validation call before allowing the record to be saved. The validation can invoke a custom action or Web API request to check for duplicates or other business rules.

Overview

We’ll use two key components:

OnSave handler — intercepts the save event and prevents it temporarily.

CheckEntity function — executes a validation call (e.g., to a custom action or an API endpoint).

If the validation finds a duplicate or issue, the user will be prompted with a confirmation dialog before proceeding.

The OnSave Function

Here’s a sample implementation of the OnSave event handler:

OnSave: function (ExecutionContext) {
    formContext = ExecutionContext.getFormContext();

    if (!SAVE) {
        ExecutionContext.getEventArgs().preventDefault();
        CustomNamespace.Entity.CheckEntity(formContext, function () {
            var confirmStrings = {
                subtitle: "Duplicate record found",
                text: "A similar record already exists. Do you want to continue saving?",
                title: "Duplicate Detected",
                confirmButtonLabel: "CONFIRM",
                cancelButtonLabel: "CANCEL"
            };
            var confirmOptions = { height: 350, width: 500 };

            Xrm.Navigation.openConfirmDialog(confirmStrings, confirmOptions).then(
                function (success) {
                    if (success.confirmed) {
                        SAVE = true;
                        formContext.data.save();
                    } else {
                        return; // cancel save
                    }
                }
            );
        }).then((existsDuplicate) => {
            if (existsDuplicate === false) {
                SAVE = true;
                formContext.data.save();
            }
        });
    }

    SAVE = false;
},

Key Notes:

  • The code uses preventDefault() to stop the default save action until validation completes.

  • It calls a validation function (CheckEntity) to verify data before saving.

  • If a potential duplicate is found, the user is shown a confirmation dialog.

  • If no duplicate exists, the record is saved automatically.

This ensures clean control over the save process and prevents unwanted record duplication.

The Validation Function

The CheckEntity function performs a validation call — for instance, invoking a custom action in Dynamics 365 that returns whether a duplicate exists.

CheckEntity: function (formContext, callback) {
    return new Promise((resolve, reject) => {
        var field1 = formContext.getAttribute("field1")?.getValue() || "";
        var field = formContext.getAttribute("field")?.getValue() || "";
        var field3 = formContext.getAttribute("field3")?.getValue() || "";
        var combinedField = (field1 + " " + field).trim();

        var lookupField = formContext.getAttribute("lookup_field")?.getValue();
        var entityId = formContext.data.entity.getId()?.replace(/[{}]/g, "") || "00000000-0000-0000-0000-000000000000";

        var entityObject = {
            "@odata.type": "Microsoft.Dynamics.CRM.entityname",
            "entityid": entityId,
            "combinedfield": combinedField,
            "field3": field3
        };

        if (lookupField && lookupField.length > 0) {
            entityObject["lookup_field@odata.bind"] = `/relatedentity(${lookupField[0].id.replace(/[{}]/g, "")})`;
        }

        var request = {
            ValidateEntityPreOperation_EntityObject: entityObject,
            getMetadata: function () {
                return {
                    boundParameter: null,
                    parameterTypes: {
                        ValidateEntityPreOperation_EntityObject: { typeName: "mscrm.entityname", structuralProperty: 5 }
                    },
                    operationType: 0,
                    operationName: "custom_ValidateEntityPreOperation"
                };
            }
        };

        Xrm.WebApi.execute(request).then(async response => {
            const contentType = response.headers.get("content-type");
            if (contentType && contentType.indexOf("application/json") !== -1) {
                var jsonResponse = await response.json();

                if (jsonResponse && callback) {
                    callback();
                    resolve(true); // duplicate found
                }
            } else {
                resolve(false); // no duplicate
            }
        }).catch(error => {
            console.error("Error in CheckEntity:", error);
            reject(null);
        });
    });
},

How to Use SpiderFoot for Effortless Cybersecurity Recon

SpiderFoot is an open-source OSINT automation tool designed to gather public information about domains, IPs, email addresses, and organizations. It automates dozens of data sources and modules so you can quickly build a comprehensive footprint of a target without manual scraping and juggling multiple tools. SpiderFoot is useful for threat intelligence, attack surface discovery, red team recon, and security assessments. (hackerhaven.io)

What SpiderFoot can do (at a glance)

  • Enumerate DNS records, subdomains, and WHOIS details.

  • Pull leaked credentials and breach data where available.

  • Search social media signals and correlate identities.

  • Discover infrastructure exposed on the internet (IP ranges, open services).

  • Export findings in JSON, CSV or visual formats for further analysis.

These capabilities make SpiderFoot an efficient first step for mapping an organization’s public attack surface.

Quick setup (local/web UI)

  1. Install or pull the repo — SpiderFoot can be run locally (CLI) or via its web UI. If you prefer an all-in-one web interface, run the server locally and open the dashboard (commonly http://127.0.0.1:5001). (InfoSec Train)

  2. Create a new scan — From the web UI click New Scan, enter the target (domain, IP, or organization name) and give it a descriptive label. (InfoSec Train)

  3. Choose a scan profile — Profiles let you balance speed vs coverage:

    • All: every module (slowest, most exhaustive).

    • Footprint: public footprinting modules only.

    • Investigate: adds malicious indicator checks.

    • Passive: avoids active probes (safer/legal for some scenarios). (InfoSec Train)

  4. Select modules and API keys — Configure modules you want (WHOIS, DNS, Shodan, HaveIBeenPwned, social lookups). Add API keys for services that require them to improve results.

  5. Run the scan and monitor — Start the scan and monitor progress in the dashboard; results stream in and are categorized by type.

Interpreting results

SpiderFoot groups findings by categories (domains, IPs, breaches, social handles, etc.). Important tips:

  • Prioritize high-confidence findings first (verified WHOIS, confirmed domain-to-IP mappings).

  • Correlate data — use timestamps, overlapping infrastructure, and repeated identifiers to join otherwise separate results.

  • Export for analysis — JSON or CSV exports let you feed results into other tools (SIEMs, graphing tools, Maltego) for deeper investigation.

Typical use cases

  • Attack surface discovery: Quickly discover subdomains, exposed services and third-party assets.

  • Phishing defense: Identify spoofable domains and leaked credentials that support targeted phishing simulations.

  • Threat intelligence: Map infrastructure and linked identities used by suspicious actors.

  • Pre engagement recon: Save time during red team or pen test engagements by automating initial discovery.

Best practices & safety

  • Use passive mode for legal safety when you don’t have authorization; active probing can trigger logging or be considered unauthorized access.

  • Respect robots.txt and API terms for external services and rate limits.

  • Limit sensitive exports — treat scan results containing personal data or breached credentials as sensitive: store securely and follow privacy rules and company policy.

  • Enrich, don’t replace — SpiderFoot is powerful, but combine its findings with human analysis and other OSINT tools (Maltego, Shodan, Recon-ng) for the full picture. (hackerhaven.io)

Example quick workflow (practical)

  1. Start SpiderFoot UI → New Scan → target example.com.

  2. Choose Footprint profile + enable WHOIS, DNS, subdomain discovery, certificate transparency modules.

  3. Run scan; export JSON.

  4. Load JSON into a graph tool or spreadsheet to group subdomains, IP ownership, and open ports.

  5. Manually validate top-risk findings and document remediation recommendations.

For hands-on walkthroughs and UI screenshots, community guides and tutorials demonstrate exact clicks and module names. (InfoSec Train)



Building a Custom API in Dynamics 365 with Entity Input and Output Parameters

The Scenario

We’ll create a Custom API ValidateExampleRecord that:

  1. Accepts an Entity as input (ValidateExampleRecord_InputEntity),

  2. Performs server-side validation and duplicate checking,

  3. Returns an Entity as output (ValidateExampleRecord_OutputEntity),

  4. Can be called from:

    • JavaScript (e.g., form OnSave),

    • Another plugin (PreOperation stage).




Create the Custom API in Dataverse

You can create it either via Power Apps Maker Portal or Power Platform CLI (PAC).

Using Power Apps Maker Portal

  1. Go to Solutions → Open or create a new solution.

  2. Click New → Automation → Custom API.

  3. Fill in the main fields as follows:



Unique NameValidateExampleRecord
Display NameValidate Example Record
Binding TypeNone (Global)
Can be called fromBoth (client and server)
Enabled for WorkflowNo
Execute Privilege Name(leave empty)
DescriptionValidates entity data and checks for duplicates.
  1. Save and publish.




Define Custom API Parameters

Next, add input and output parameters.

Input Parameter



NameValidateExampleRecord_InputEntity
Display NameInput Entity
TypeEntity
Entity Logical Nameexample_entity
Is OptionalNo
DirectionInput

Output Parameter



NameValidateExampleRecord_OutputEntity
Display NameOutput Entity
TypeEntity
Entity Logical Nameexample_entity
DirectionOutput

Then save and publish again.



Custom API Plugin (Server-Side Validation)


Here’s a sample C# plugin class that implements the logic behind our Custom API.
It accepts an entity as input, validates its content, and outputs a
matching entity if found.

using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Example.CustomAPIs
{
    public class ValidateExampleRecord : IPlugin
    {
        ITracingService tracing;

        public void Execute(IServiceProvider serviceProvider)
        {
            tracing =
(ITracingService)serviceProvider.GetService(typeof(ITracingService));
            var context =
(IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            var serviceFactory =
(IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            var service = serviceFactory.CreateOrganizationService(null);

            tracing.Trace("START ValidateExampleRecord");

            try
            {
                if
(!context.InputParameters.Contains("ValidateExampleRecord_InputEntity")
||

context.InputParameters["ValidateExampleRecord_InputEntity"] == null)
                    throw new InvalidPluginExecutionException("The
input parameter 'InputEntity' is missing or null.");

                var inputEntity =
(Entity)context.InputParameters["ValidateExampleRecord_InputEntity"];
                tracing.Trace($"Entity: {inputEntity.LogicalName}, ID:
{inputEntity.Id}");

                // Extract attributes
                var recordName =
inputEntity.GetAttributeValue<string>("example_name");
                var recordCode =
inputEntity.GetAttributeValue<string>("example_identifier");
                var parentRefId =
inputEntity.GetAttributeValue<EntityReference>("example_parentreference")?.Id;

                // Validate required attributes
                var missing = new List<string>();
                if (string.IsNullOrWhiteSpace(recordName))
missing.Add("example_name");
                if (string.IsNullOrWhiteSpace(recordCode))
missing.Add("example_identifier");
                if (parentRefId == null || parentRefId == Guid.Empty)
missing.Add("example_parentreference");

                if (missing.Count > 0)
                    throw new
InvalidPluginExecutionException($"Missing required attributes:
{string.Join(", ", missing)}");

                // Run duplicate search
                var existingRecord = FindExisting(service,
inputEntity.Id, recordName, recordCode, parentRefId.Value);

context.OutputParameters["ValidateExampleRecord_OutputEntity"] =
existingRecord;

                tracing.Trace("END ValidateExampleRecord");
            }
            catch (Exception ex)
            {
                tracing.Trace($"Error: {ex}");
                throw new InvalidPluginExecutionException($"Custom API
error: {ex.Message}", ex);
            }
        }

        private Entity FindExisting(IOrganizationService service,
Guid? entityId, string name, string code, Guid parentId)
        {
            tracing.Trace("Executing duplicate search...");
            Entity result = null;

            var query = new QueryExpression("example_entity")
            {
                ColumnSet = new ColumnSet("example_name",
"example_identifier", "example_parentreference")
            };

            query.Criteria.AddCondition("example_name",
ConditionOperator.Equal, name);
            query.Criteria.AddCondition("example_parentreference",
ConditionOperator.Equal, parentId);
            if (entityId != null && entityId != Guid.Empty)
                query.Criteria.AddCondition("example_entityid",
ConditionOperator.NotEqual, entityId);

            var matches = service.RetrieveMultiple(query).Entities;
            tracing.Trace($"Found {matches.Count} possible duplicates.");

            var existing = matches.FirstOrDefault();
            if (existing != null)
            {
                var existingCode =
existing.GetAttributeValue<string>("example_identifier");
                if (!string.IsNullOrWhiteSpace(existingCode) &&
                    string.Equals(existingCode.Trim(), code.Trim(),
StringComparison.OrdinalIgnoreCase))
                {
                    result = existing;
                    tracing.Trace("Duplicate record found.");
                }
            }

            return result;
        }
    }
}

Calling the Custom API from JavaScript


You can trigger this Custom API from the form’s OnSave event to
prevent users from creating duplicates.
This example uses Xrm.WebApi.execute() to send the entity data to the
Custom API.

var formContext;
var ALLOW_SAVE = false;

if (typeof (APP) == "undefined") {
    APP = {};
}

APP.Record = {

    OnSave: function (ExecutionContext) {
        formContext = ExecutionContext.getFormContext();

        if (!ALLOW_SAVE) {
            APP.Record.CheckRecord(formContext, function () {
                var dialogOptions = {
                    title: "Duplicate Detected",
                    text: "A record with similar values already
exists. Do you want to continue?",
                    confirmButtonLabel: "Confirm",
                    cancelButtonLabel: "Cancel"
                };


Xrm.Navigation.openConfirmDialog(dialogOptions).then(function
(response) {
                    if (response.confirmed) {
                        ALLOW_SAVE = true;
                        formContext.data.save();
                    } else {
                        ExecutionContext.getEventArgs().preventDefault();
                    }
                });
            });
        }

        ALLOW_SAVE = false;
    },

    CheckRecord: function (formContext, callback) {
        var name = formContext.getAttribute("example_name")?.getValue() || "";
        var code =
formContext.getAttribute("example_identifier")?.getValue() || "";
        var parent =
formContext.getAttribute("example_parentreference")?.getValue();
        var recordId =
formContext.data.entity.getId()?.replace(/[{}]/g, "") ||
"00000000-0000-0000-0000-000000000000";

        var inputEntity = {
            "@odata.type": "Microsoft.Dynamics.CRM.example_entity",
            "example_entityid": recordId,
            "example_name": name,
            "example_identifier": code
        };

        if (parent && parent.length > 0) {
            inputEntity["example_parentreference@odata.bind"] =
`/example_parents(${parent[0].id.replace(/[{}]/g, "")})`;
        }

        var request = {
            ValidateExampleRecord_InputEntity: inputEntity,
            getMetadata: function () {
                return {
                    boundParameter: null,
                    parameterTypes: {
                        ValidateExampleRecord_InputEntity: { typeName:
"mscrm.example_entity", structuralProperty: 5 }
                    },
                    operationType: 0,
                    operationName: "ValidateExampleRecord"
                };
            }
        };

        Xrm.WebApi.execute(request)
            .then(async response => {
                if (!response.ok) throw new Error("HTTP Error: " +
response.status);
                const type = response.headers.get("content-type");
                return type && type.indexOf("application/json") !== -1
? await response.json() : null;
            })
            .then(result => {
                if (result) callback();
            })
            .catch(error => console.error("Error in CheckRecord:", error));
    }
};

Calling the Custom API from Another Plugin (with Target + PreImage)

In this version, the plugin executes during the Pre-Operation stage
and uses both the incoming Target entity (the data being updated or
created) and a PreImage (the record as it was before the update).
This ensures consistent behavior whether the operation is a Create or Update.

using Microsoft.Xrm.Sdk;
using System;

namespace Example.Plugins
{
    public class ExampleRecordPreOperation : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            var tracing =
(ITracingService)serviceProvider.GetService(typeof(ITracingService));
            var context =
(IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            var serviceFactory =
(IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            var service =
serviceFactory.CreateOrganizationService(context.InitiatingUserId);

            tracing.Trace("START - ExampleRecordPreOperation");

            // Ensure plugin registration context
            if (context.Stage != 20 || (context.MessageName !=
"Create" && context.MessageName != "Update"))
            {
                tracing.Trace($"Unexpected context:
Stage={context.Stage}, Message={context.MessageName}");
                return;
            }

            // Get target entity
            if (!context.InputParameters.Contains("Target") ||
!(context.InputParameters["Target"] is Entity target))
                throw new InvalidPluginExecutionException("Target
entity is missing or invalid.");

            // Get PreImage for Update
            Entity preImage = null;
            if (context.MessageName == "Update" &&
context.PreEntityImages.Contains("PreImage"))
                preImage = context.PreEntityImages["PreImage"];

            // Merge data between target and preImage
            string recordName = target.GetAttributeValue<string>("example_name")
                                ??
preImage?.GetAttributeValue<string>("example_name");

            string recordCode =
target.GetAttributeValue<string>("example_identifier")
                                ??
preImage?.GetAttributeValue<string>("example_identifier");

            EntityReference parentRef =
target.GetAttributeValue<EntityReference>("example_parentreference")
                                        ??
preImage?.GetAttributeValue<EntityReference>("example_parentreference");

            // Prepare the entity for Custom API validation
            var entityToValidate = new Entity("example_entity");

            if (context.MessageName == "Update" && target.Id != Guid.Empty)
                entityToValidate.Id = target.Id;

            if (!string.IsNullOrWhiteSpace(recordName))
                entityToValidate["example_name"] = recordName;

            if (!string.IsNullOrWhiteSpace(recordCode))
                entityToValidate["example_identifier"] = recordCode;

            if (parentRef != null)
                entityToValidate["example_parentreference"] = parentRef;

            tracing.Trace("Calling Custom API: ValidateExampleRecord");

            // Execute Custom API
            var request = new OrganizationRequest("ValidateExampleRecord")
            {
                ["ValidateExampleRecord_InputEntity"] = entityToValidate
            };

            var response = service.Execute(request);
            var duplicate =
response.Results["ValidateExampleRecord_OutputEntity"] as Entity;

            // Handle duplicate scenario
            if (duplicate != null && duplicate.Id != Guid.Empty)
                throw new InvalidPluginExecutionException(
                    "A record with the same name and identifier
already exists for this parent record.");

            tracing.Trace("END - ExampleRecordPreOperation");
        }
    }
}


Optional: Registering the Plugin

In your plugin registration tool (e.g., Plugin Registration Tool or
Power Platform CLI):

Stage: Pre-Operation (20)

Messages: Create and Update

Primary Entity: example_entity

PreImage Name: PreImage

PreImage Attributes: example_name, example_identifier, example_parentreference