npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xrmforge/helpers

v0.17.0

Published

Browser-safe runtime helpers for Dynamics 365 form scripts: select(), parseLookup(), typedForm(), Xrm constants, Action/Function executors

Readme

@xrmforge/helpers

npm version license

Browser-safe runtime helpers for Dynamics 365 form scripts. Zero Node.js dependencies, safe to bundle into esbuild IIFE Web Resources. Pairs with the types generated by @xrmforge/typegen to eliminate raw strings and magic values from your form code.

Part of XrmForge. This is the package you import in actual D365 form scripts.


Installation

npm install @xrmforge/helpers

Install as a regular dependency (not devDependency): generated Custom API action files import the action factories from this package at runtime.

Requirements: @types/xrm (>= 9.0.0) as a peer dependency.


What you get

| Area | Exports | |------|---------| | Web API query strings | select, selectExpand, parseLookup, parseLookups, parseFormattedValue, parseMultiSelect, expanded, expandedMany | | Form lookups | formLookup, formLookupId, formLookupIdUnsafe, formLookupUnsafe | | Typed form proxy | typedForm, normalizeGuid, isUnsavedRecord, types TypedForm, FormFields, FormTypeInfoProtocol | | Xrm constants (const enum) | DisplayState, FormType, isFormType, FormNotificationLevel, AppNotificationLevel, RequiredLevel, SubmitMode, SaveMode, ClientType, ClientState, OperationType, StructuralProperty, BindingType | | Custom API runtime | createBoundAction, createUnboundAction, createBoundFunction, createUnboundFunction, executeRequest, executeMultiple | | Attribute submit | setAndSubmit, setUnsafeAndSubmit, clearAndSubmit | | App notifications | addAppNotification, clearAppNotification | | HTML WebResource context | parentXrm, getWebResourceContext | | Environment variables | getEnvironmentVariable, clearEnvironmentVariableCache | | Convenience | withProgress, callCloudFlow |


TypedForm: direct, typed field access

typedForm() wraps a FormContext so you can read and write fields as properties instead of getAttribute("...") chains. Pass the generated <Form>TypeInfo type (typegen >= 0.10.0) -- it bundles the field/attribute/control maps so type extraction works reliably across package boundaries.

import { typedForm } from '@xrmforge/helpers';
import type { AccountMainFormTypeInfo } from '../../generated/forms/account.js';

export function onLoad(ctx: Xrm.Events.EventContext): void {
  const form = typedForm<AccountMainFormTypeInfo>(ctx.getFormContext());

  form.name.getValue();              // string | null (typed StringAttribute)
  form.revenue.setValue(150000);     // typed NumberAttribute

  form.controls.name.setDisabled(true);          // typed control access
  form.$context.ui.tabs.get('SUMMARY_TAB');       // full FormContext escape hatch
  form.$unsafe('off_form_field')?.setValue('x');  // off-form field (nullable)
}

setValue() on the proxy automatically calls setSubmitMode('always'), so programmatically set values are not silently dropped by D365 AutoSave. Assigning to a field directly (form.name = ...) throws a TypeError telling you to use .setValue().


Web API helpers

Build OData query strings from generated Fields enums and parse responses without hand-written annotation keys.

import { select, parseLookup, parseFormattedValue, parseMultiSelect } from '@xrmforge/helpers';
import { AccountFields } from '../../generated/fields/account.js';
import { AccountNavigationProperties as Nav } from '../../generated/entities/account.js';

const result = await Xrm.WebApi.retrieveRecord('account', id,
  select(AccountFields.Name, AccountFields.WebsiteUrl, AccountFields.PrimaryContact));

const contact = parseLookup(result, Nav.PrimaryContact);   // { id, name, entityType } | null
const status  = parseFormattedValue(result, 'statecode');  // "Active" instead of 0
const types   = parseMultiSelect(result.markant_typesmulticode); // normalize to number[]

formLookup() / formLookupId() extract a lookup (with a brace-normalized GUID) straight from a form attribute:

import { formLookupId } from '@xrmforge/helpers';
const accountId = formLookupId(form.getAttribute(Fields.AccountId)); // string | null

Xrm constants instead of raw strings

@types/xrm types these values as string literals but provides no runtime constants (XrmEnum does not exist at runtime under esbuild). These const enums are inlined at compile time -- zero runtime cost, no XrmEnum is not defined surprises.

import { DisplayState, FormNotificationLevel, RequiredLevel, SubmitMode, SaveMode } from '@xrmforge/helpers';

tab.setDisplayState(DisplayState.Expanded);
form.$context.ui.setFormNotification('Saved.', FormNotificationLevel.Info, 'saved');
form.email.setRequiredLevel(RequiredLevel.Required);

Comparing form types needs care: getFormType() is typed as the distinct XrmEnum.FormType, so a direct === fails to compile under strict. Use isFormType():

import { isFormType, FormType } from '@xrmforge/helpers';
if (isFormType(form.$context, FormType.Create)) { /* only on create */ }

Custom API executors

Generated action files (in generated/actions/) use the runtime factories from this package. Each executor exposes .execute() (calls Xrm.WebApi) and .request() (for batching).

import { WinQuote } from '../generated/actions/quote';
import { ValidateMandatoryFields } from '../generated/actions/global';

// Bound action: void result throws on failure, so just await
await WinQuote.execute(quoteId);

// Unbound action with typed params and response
const result = await ValidateMandatoryFields.execute({ TargetId: id, RuleSet: 'Full', Language: 'de' });
if (!result.IsValid) alert(result.MessageDe);

Batch multiple calls in one round-trip with executeMultiple and .request():

import { executeMultiple } from '@xrmforge/helpers';
await executeMultiple([
  ApproveRecord.request({ RecordId: id }),
  NotifyOwner.request({ RecordId: id, Message: 'Approved' }),
]);

You can also build executors by hand with createBoundAction / createUnboundAction / createBoundFunction / createUnboundFunction.


Convenience helpers

import { withProgress, callCloudFlow, clearAndSubmit, setUnsafeAndSubmit, addAppNotification, clearAppNotification, AppNotificationLevel } from '@xrmforge/helpers';

// Progress spinner around an async operation (errors propagate to your handler wrapper)
await withProgress('Processing...', () => WinQuote.execute(quoteId));

// Typed Power Automate cloud-flow call (HTTP-trigger URL from configuration, never hard-coded)
const price = await callCloudFlow<{ quoteId: string }, { total: number }>(FLOW_URL, { quoteId });

// Clear / set an attribute and force it to submit (avoids silent AutoSave drops)
clearAndSubmit(form.revenue);
setUnsafeAndSubmit(form, OpportunityFields.VslBeauftragung, closeDate);

// Global banner notification - addAppNotification returns the id, clearAppNotification removes it.
// (Use { autoHideMs } for a self-clearing banner; clearAppNotification handles the persistent case.)
const id = await addAppNotification('Saved.', AppNotificationLevel.Success, { showCloseButton: true });
await clearAppNotification(id);

HTML WebResource context

An embedded HTML WebResource (a page in a form IFrame or the sitemap) gets no executionContext. These helpers reach the host form API and the hosting record without hand-casting window.parent or the page context:

import { parentXrm, getWebResourceContext, select } from '@xrmforge/helpers';

const { entityId, entityName } = getWebResourceContext();   // hosting record (brace-stripped id)
if (entityId) {
  const record = await parentXrm().WebApi.retrieveRecord(entityName, entityId, select(/* ... */));
}

Both are browser-safe (zero Node.js dependencies, fine in esbuild IIFE bundles).


Documentation

Full guide and patterns: XrmForge on GitHub.

License

MIT (c) XrmForge Contributors.