Everything so far in this series has stayed on the form: fields, events, lookups, subgrids. This post steps off the form and into Dataverse directly, using the Web API to read and write records the form doesn't already have loaded — the foundation you need before the next post, where this starts connecting to Copilot Studio agents.
Why Call the Web API from a Form Script
A few common reasons: pulling in data that isn't on the form (like a related account's credit limit), creating a related record without navigating the user away, running a real-time check against other records before save, or calling a custom Dataverse action. All of it goes through Xrm.WebApi, and every method returns a Promise.
The Basics: Xrm.WebApi
// Read one record
Xrm.WebApi.retrieveRecord("account", accountId, "?$select=name,creditlimit");
// Read multiple records
Xrm.WebApi.retrieveMultipleRecords("contact", "?$select=fullname&$filter=parentcustomerid_value eq " + accountId);
// Create a record
Xrm.WebApi.createRecord("task", {
subject: "Follow up",
"regardingobjectid_account@odata.bind": "/accounts(" + accountId + ")"
});
// Update a record
Xrm.WebApi.updateRecord("account", accountId, { creditlimit: 50000 });
Note the entity set names are plural and lowercase (accounts, contacts), but retrieveRecord/createRecord/updateRecord take the singular logical name (account) — it's a common source of confusion.
Reading a Related Record
Here's a real example: when a contact form loads, pull the parent account's credit limit and show it in a notification:
async function onLoad(executionContext) {
var formContext = executionContext.getFormContext();
var accountValue = formContext.getAttribute("parentcustomerid").getValue();
if (!accountValue) return;
var accountId = accountValue[0].id.replace("{", "").replace("}", "");
try {
var result = await Xrm.WebApi.retrieveRecord("account", accountId, "?$select=creditlimit");
formContext.ui.setFormNotification(
"Account credit limit: " + (result.creditlimit || 0),
"INFO",
"credit_limit_info"
);
} catch (error) {
console.error("Failed to retrieve account credit limit", error);
}
}
Creating a Related Record
async function logFollowUpTask(executionContext) {
var formContext = executionContext.getFormContext();
var contactId = formContext.data.entity.getId().replace("{", "").replace("}", "");
var task = {
subject: "Follow up on discount request",
"regardingobjectid_contact@odata.bind": "/contacts(" + contactId + ")"
};
try {
await Xrm.WebApi.createRecord("task", task);
} catch (error) {
console.error("Failed to create follow-up task", error);
}
}
A Practical Example: Real-Time Duplicate Check on Save
Combining an async Web API call with OnSave, including blocking the save until the check completes:
formContext.data.entity.addOnSave(onSaveCheckDuplicate);
async function onSaveCheckDuplicate(executionContext) {
var eventArgs = executionContext.getEventArgs();
var formContext = executionContext.getFormContext();
var email = formContext.getAttribute("emailaddress1").getValue();
if (!email) return;
eventArgs.preventDefault(); // pause the save while we check
try {
var query = "?$select=contactid&$filter=emailaddress1 eq '" + email + "'";
var result = await Xrm.WebApi.retrieveMultipleRecords("contact", query);
var isDuplicate = result.entities.some(function (c) {
return c.contactid !== formContext.data.entity.getId().replace("{", "").replace("}", "");
});
if (isDuplicate) {
formContext.ui.setFormNotification("A contact with this email already exists.", "ERROR", "dup_check");
} else {
formContext.data.entity.save(); // safe to save now
}
} catch (error) {
console.error("Duplicate check failed", error);
formContext.data.entity.save(); // don't block save on a broken check
}
}
Common Pitfalls
- Forgetting await. Without it, your code moves on before the API call resolves, and you end up reading undefined data or saving before a check completes.
- Not handling rejected promises. A network error or a security-role denial throws — always wrap calls in try/catch.
- Wrong entity set name. retrieveMultipleRecords needs the plural set name; retrieveRecord/createRecord/updateRecord need the singular logical name.
- Too many calls per save. Each Web API call is a real HTTP request; batch or cache where you can instead of calling it inside a loop.
What's Next
Last post in this series: wiring a Copilot Studio agent action into a JavaScript-driven ribbon button — where form scripting and agents meet.
No comments:
Post a Comment