JavaScript Scripts
Custom JavaScript functions extend the form where rules are not enough. Scripts must be enabled by a tenant administrator.
A script is JavaScript that runs in the browser of every user who opens the form, with that user's permissions, and is not sandboxed. Anyone allowed to modify the form configuration can execute arbitrary code. Use scripts only when rules and expressions are insufficient, and review the code carefully.
When to use a script
A script is a last resort. Try these tools first, because they are safer, do not require tenant-level enablement, and can be validated by the designer:
- Rules: visibility, required state, read-only locking, colors, banners, blocking save.
- Expressions: calculations and string concatenation.
- Custom buttons: bulk value updates with a single click.
Scripts are suitable for complex logic (loops, multiple conditions, working with arrays and objects), custom messages based on form state, and checks that cannot be expressed as rule conditions.
Script policy (tenant enablement)
Scripts are disabled by default. A tenant administrator enables them once for all sites (tenant Storage Entity FormCraft365_ScriptsPolicy = allow; the legacy key SpfxFormCustomizer_ScriptPolicy = enabled is only read when the new key does not exist, and deny in the new key always takes precedence). A missing or unknown value means disabled. Details: Script Policy.
- Enabling: in the designer under Settings → Security → Custom scripts using the toggle if you have App Catalog administrator permissions; otherwise the designer displays the
Set-PnPStorageEntitycommand for an administrator. The change takes effect after reloading the form, within 15 minutes at the latest. - An error or missing value means disabled.
- When disabled, scripts are not even compiled. Neither
onFormLoad,onBeforeSave, nor any named function runs. Rules, expressions, and non-script button actions continue to work. - Consequently,
onBeforeSavedoes not block saving when scripts are disabled. To block saving reliably, use a blockSave rule. - The designer (and only the designer) sees a notice at the top of the form stating that scripts are disabled. The same warning appears in the script editor.
Script editor
Without a Pro license, the editor is read-only: an explanation with a Pro badge is shown at the top, existing scripts can be viewed but not edited, and new functions cannot be added. Scripts still run (only the script policy determines execution). See Licensing.
In the designer, open the Scripts tab. It contains three sections and a reference panel for ctx:
- On form load script (
onFormLoad): code that runs once after the form's default values are loaded. - Before save script (
onBeforeSave): code executed before submitting to SharePoint. - Custom named functions (
functions): functions callable from rules and buttons. The function name must be a valid JavaScript identifier (e.g.recalculateVat) and unique within the form.
Click Test syntax to verify that the code compiles. Write the code as a function body, not a declaration: do not wrap it in function name() {…}. The body runs in strict mode as an async function, so you can use await and return. The only parameter is ctx.
Load and before-save scripts
onFormLoad
Runs once after the form loads (and again if the form mode or rule configuration changes). Useful for a welcome message, pre-filling values, or initially hiding fields. The form does not wait for this script and renders immediately. Any return value is ignored.
onBeforeSave
Runs on Save after field validation and after onSave rules. Its return value determines whether saving proceeds:
- Returns a non-empty string (
return "Please enter a price."): saving stops and the string is displayed as an error message at the top of the form. - Returns
false: saving stops. If the script previously reported an error viactx.notify(text, "error"), that message remains visible. Otherwise, a generic message "Form saving was stopped by a validation script." is shown in the form's language. - Anything else (
true,undefined, noreturn): saving continues. - A code error (exception, invalid syntax, blocked
unsafe-eval) stops saving and displays the actual error to the user, such as "Script error: Cannot read properties of undefined". The full output including stack trace is logged to the browser console.
if (!ctx.getValue("Cena")) return "Please enter the price before saving.";
const total = Number(ctx.getValue("Cena")) * Number(ctx.getValue("Kusy") || 1);
if (total > 100000 && !ctx.getValue("Schvalovatel")) {
return "Orders over 100,000 must have an approver.";
}
Named functions
You define functions in the editor and call them using the callFunction action in a rule (target $script) or the callFunction action on a button. In the designer, the rule lets you pick from defined function names; configuration validation rejects references to non-existent functions. The function's return value is ignored. A function called from an always rule runs when its conditions become true or their values change, up to 10 times (loop protection).
{
"scripts": {
"functions": [
{
"id": "fn-dph",
"name": "prepocitejDph",
"description": "Calculates price including VAT",
"code": "const bez = Number(ctx.getValue('CenaBezDph')) || 0;\nctx.setValue('CenaSDph', Math.round(bez * 1.21 * 100) / 100);"
}
]
}
}
Init vs. user phase: when the form is "dirty"
Scripts run in two phases, which affects how ctx.setValue behaves:
| Phase | When | Default shouldDirty for ctx.setValue |
|---|---|---|
| init | onFormLoad, functions called from onLoad rules, and the initial rule evaluation on open | false: the form is not marked as modified |
| user | onBeforeSave, functions from onSave rules, buttons, and subsequent field changes | true: the form is marked as modified |
- This ensures that merely opening the form does not trigger an unsaved-changes warning, even if
onFormLoadpre-fills values. - A value set in the init phase (without marking dirty) is still saved by the form, even in Edit mode where otherwise only modified fields are saved. This applies only to values that differ from the current value.
- You can override the default behavior with the third argument:
ctx.setValue(name, value, { shouldDirty: true }). - In display mode, the form is never marked as modified and values are not saved.
Calling from rules and buttons
- From a rule: according to the trigger on change (
always), after load (onLoad), or on save beforeonBeforeSave(onSave). - From a button: on click, in sequence with other actions.
- When scripts are disabled, the call is skipped and other actions proceed.
Async code, errors, and debugging
- The body is
async. You can useawait(for exampleawait fetch(...)), though the form does not wait foronFormLoad.onBeforeSaveand functions from buttons andonSaverules are awaited. - Errors do not crash the form. They appear as a red banner at the top of the form: Script compilation error, Script error: … with the exception message, or a notice about blocked
unsafe-eval(see Script Policy). Details are in the browser console (F12). - Debug mode: append
?fcdebug=1to the URL (or enable debugMode). The console then logs each script's name (such asonFormLoadorrule 'X' callFunction 'Y'), duration, return value,ctx.notifymessages, any errors, and the source of modified fields ("script:init (nedirty)"). A script skipped due to a disabled policy is also noted in the log. - You can also write to the console from a script using
console.log(...).
Script examples
Welcome message by mode (onFormLoad)
if (ctx.formMode === "new") {
ctx.notify("Welcome, " + (ctx.currentUser?.title || "") + ". Please fill in all required fields.", "info");
}
Pre-filling a value on a new item (onFormLoad)
if (ctx.formMode === "new" && !ctx.getValue("Oddeleni")) {
ctx.setValue("Oddeleni", "Sales"); // init phase: form remains clean
}
Hiding a section by group (onFormLoad)
const groups = (ctx.currentUser?.groups || []).map(g => g.toLowerCase());
if (groups.indexOf("approvers") < 0) {
ctx.setVisible("section:sec_schvaleni", false);
}
Recalculation on button click (named function)
const bez = Number(ctx.getValue("CenaBezDph")) || 0;
ctx.setValue("CenaSDph", Math.round(bez * 1.21 * 100) / 100);
ctx.notify("Price including VAT has been recalculated.", "success");
Validation before save (onBeforeSave)
const from = ctx.getValue("Od");
const to = ctx.getValue("Do");
if (from && to && String(to) < String(from)) {
return "The 'To' date cannot be earlier than the 'From' date.";
}
Security notice
A script has the same permissions as the signed-in user; it can read and modify data, call SharePoint, and call other services. Enabling scripts applies to the entire tenant.
- Do not paste code you do not understand or code from untrusted sources.
- Only site owners and optionally a designated designers group may change the configuration. Before locking storage, the designer shows a summary of scripts contained in the configuration. Review them.
- A script does not run only when you open the form—it runs for every user who opens it. Any bug affects everyone.
- Consider whether enabling scripts on the tenant is truly necessary. Most needs are covered by rules and expressions, which do not require scripts.
Context API ctx
Every script receives the ctx object. Read-only members cannot be modified. The target parameter for setVisible and setReadOnly is a field internal name, section:<section id>, or tab:<tab id>. States set by a script reset to their defaults when the form mode or rule configuration changes.
ctx.formMode
Type: "new" | "edit" | "display" (read-only). Current form mode: new item, edit, or display.
if (ctx.formMode === "display") return;
ctx.itemId
Type: number | undefined (read-only). ID of the item being edited. For a new item (not yet saved), it is undefined.
if (ctx.itemId) console.log("Editing item", ctx.itemId);
ctx.locale
Type: string | undefined (read-only). Code of the currently selected form language (cs, en, de, sk). Updates when the user switches languages.
ctx.notify(ctx.locale === "en" ? "Saved draft" : "Koncept uložen", "info");
ctx.currentUser
Type: { id?, email?, loginName?, title?, groups? } | undefined (read-only). Signed-in user: ID, email, login name, display name, and names of SharePoint groups they belong to (groups).
const isApprover = (ctx.currentUser?.groups || []).indexOf("Approvers") >= 0;
ctx.getValue
Type: (fieldName: string) => unknown. Returns the current value of a field by its internal name. The value type depends on the field: string, number, boolean, date as string, or an object / array of objects for person, lookup, and managed metadata fields. Non-existent fields return undefined.
const price = Number(ctx.getValue("Cena")) || 0;
ctx.getAllValues
Type: () => Record<string, unknown>. Returns a copy of all current form values (modifying the copy does not affect the form).
const all = ctx.getAllValues();
console.log(Object.keys(all).length + " fields");
ctx.setValue
Type: (fieldName: string, value: unknown, options?: { shouldDirty?: boolean; shouldValidate?: boolean }) => void. Sets a field's value. shouldValidate defaults to true (the field is validated immediately). shouldDirty depends on the phase: false in init, true in user, and always false in display mode. Pass the value in the shape the field expects (number, string, or an object with id and title for person or lookup fields).
ctx.setValue("Stav", "Approved");
ctx.setValue("Poznamka", "", { shouldDirty: true });
ctx.setVisible
Type: (target: string, visible: boolean) => void. Shows or hides a field, section (section:id), or tab (tab:id). The script overrides the rule-derived state for that target.
ctx.setVisible("tab:tab_priloha", false);
ctx.setRequired
Type: (fieldName: string, required: boolean) => void. Sets the required state of a field (fields only, not sections or tabs).
ctx.setRequired("DuvodZamitnuti", ctx.getValue("Stav") === "Rejected");
ctx.setReadOnly
Type: (target: string, readOnly: boolean) => void. Locks or unlocks a field, section (section:id), or tab (tab:id). In display mode, all fields remain read-only.
ctx.setReadOnly("section:sec_cena", true);
ctx.setCollapsed
Type: (sectionId: string, collapsed: boolean) => void. Collapses or expands a section. You may pass the ID with or without the section: prefix. Works on sections configured as collapsible; the user can toggle it manually afterward.
ctx.setCollapsed("sec_prilohy", true);
ctx.notify
Type: (message: string, intent?: "info" | "success" | "warning" | "error") => void. Displays a banner message at the top of the form. The default intent is info. Only one message is shown at a time: a new one replaces the previous one, and the user can dismiss it.
ctx.notify("Warning: the due date is in the past.", "warning");