Appify CPO lets you add custom JavaScript for advanced behavior beyond built-in configuration. There are two script types:
| Script type | When it runs | Best for |
|---|---|---|
| Global script | Once, automatically, when this calculator is ready (you do not listen for lifecycle events) | Setup: analytics, DOM helpers, third-party widgets |
| Option script | When that option’s value changes (and once on attach with the current value if the calculator already emitted) | Reactive logic tied to a specific field |
Configure them in Calculator Editor → Placement & Styling → JavaScript.
Custom scripts run in the storefront page context (not inside the calculator Shadow DOM). Use ctx.host / ctx.root to work inside the calculator instance.
ctx, save.ctx, save.Test thoroughly before publishing — script errors can break the shopper experience.
The editor prefills a short comment describing ctx. Your code runs once automatically when the calculator is ready for custom JS. You do not need:
document.addEventListener("calc:ready", ...);
document.addEventListener("calc:customJsReady", ...);
inside a Global script — Appify waits for the calculator, then runs your code.
ctx| Field | Type | Description |
|---|---|---|
calculatorId |
string | null |
Calculator id |
instanceId |
string | null |
Instance id (multi-calculator pages) |
productId |
string | number | null |
Product id when linked to a product |
host |
Element | null |
[data-calcai-calculator] host element |
root |
ShadowRoot | Element | null |
Shadow root (or host) for DOM queries |
setValue(optionId, value) |
function |
Set an option on this calculator via window.CalcAI |
console.log("Calculator ready", ctx.calculatorId);
// Optional: keep listening for later shopper changes
document.addEventListener("calc:change", (e) => {
const d = e.detail || {};
if (d.calculatorId !== ctx.calculatorId) return;
if (d.external) return;
// d.values, d.totals, ...
});
After the Global script has run (or immediately if the Global script is empty), Appify dispatches:
document.addEventListener("calc:customJsReady", (e) => {
const { calculatorId, instanceId, host, root } = e.detail || {};
if (calculatorId !== "YOUR_CALC_ID") return;
// Safe: calculator is rendered and calculator custom JS has finished startup
});
Helpers (when available):
window.CalcAI.isCustomJsReady("YOUR_CALC_ID")window.CalcAI.getCustomJsReady("YOUR_CALC_ID") → last detail or nullAlso see Storefront JavaScript API for window.CalcAI.setValue and calc:change payloads.
ctxOption scripts are change handlers. The editor prefills a comment describing ctx. Your code runs inside a function that receives ctx whenever that option’s value changes, and once on attach with the current value if the calculator already emitted before the script loaded.
ctx shape?Yes for the top-level keys. Every option script gets the same ctx fields (value, prevValue, values, totals, option, …).
What varies by option type is the type/shape of ctx.value and ctx.prevValue (and the matching entry inside ctx.values).
ctx reference| Field | Type | Description |
|---|---|---|
value |
varies | New value for this option |
prevValue |
varies | undefined |
Previous value (undefined on the first run) |
values |
object |
Map of all option keys → current values |
totals |
object | null |
Pricing snapshot (see below) |
summary |
object | null |
Price-summary payload used by summary blocks |
option |
object |
{ id, key, label, type } for this option |
optionKey |
string |
This option’s key |
calculatorId |
string | null |
Calculator id |
instanceId |
string | null |
Instance id (multi-calculator pages) |
productId |
string | number | null |
Product id when linked to a product |
reason |
string | null |
e.g. "recompute", "draw", "external-set" |
external |
boolean |
true if the change came from CalcAI.setValue / API |
host |
Element | null |
[data-calcai-calculator] host element |
root |
ShadowRoot | Element | null |
Shadow root (or host) for DOM queries inside the calculator |
setValue(optionId, value) |
function |
Sets another option on this calculator via window.CalcAI |
ctx.value / ctx.prevValue)Option type |
Typical value |
|---|---|
dropdown, select, radio, button, swatch, image-swatch |
String choice token |
switch |
Boolean |
number, slider, quantity |
Number |
text, textarea, email, color, … |
String |
checkbox-group / multi-select style |
Array of strings |
dimensions, length, area, volume, weight |
Object |
ctx.values uses the same per-key shapes for every option on the calculator.
We document stable top-level fields and the common nested keys merchants need. We do not freeze every nested property of totals / summary / full calculator spec in the editor comment — those can grow as pricing features evolve.
ctx.totals (common fields):
| Field | Meaning |
|---|---|
subtotal |
Product line subtotal (before/with calculator discount rules as computed) |
discountAmount |
Calculator discount amount |
discountPercent |
Percent when applicable, else null |
total |
Final calculator total (includes order fees when configured) |
orderFees |
Array of { id, name, amount } |
orderFeeTotal |
Sum of order fees |
unitPrice |
Unit-price display info (value/raw/label/…) when available |
ctx.values: plain object keyed by option key, e.g. { WIDTH: 48, COLOR: "red" }.
For storefront events (calc:ready, calc:change) and window.CalcAI.setValue, see Storefront JavaScript API.
// Attached to option key WIDTH
if (ctx.external) return;
const n = Number(ctx.value);
if (!Number.isFinite(n)) return;
if (n > 96) {
console.warn(ctx.option.label + " exceeds manufacturing max", n);
}
// Example: push a related option
// ctx.setValue("PRICE_TIER", n > 48 ? "large" : "standard");
const host = ctx.host; // [data-calcai-calculator]
const root = ctx.root; // usually host.shadowRoot
const label = root?.querySelector?.(
`#calc-${ctx.calculatorId}--label--${ctx.optionKey}`
);
calc:ready.calc:change reports a new value for that option key. If the script file loads after the calculator already emitted values, it runs once immediately with the current value (catch-up), then on later changes. Identical values are not re-fired.ctx.external / reason === "external-set" to avoid feedback loops when you call ctx.setValue.calc:customJsReady (or CalcAI.isCustomJsReady) instead of calc:ready if they depend on calculator custom JS having finished startup.