Documentation Index
Fetch the complete documentation index at: https://docs.launchmystore.io/llms.txt
Use this file to discover all available pages before exploring further.
Input field selection
Functions can declare exactly which REST input fields they need on
every invocation. The dispatcher trims the assembled input to that
subtree before invoking your WASM module — your function sees a smaller,
faster, exactly-shaped object.
This is purely REST: you author a plain JSON tree of booleans matching
the input contract for your function type. There is no GraphQL involved.
Why opt in
- Faster dispatch. Smaller objects serialize to JSON faster; smaller
payloads cross the WASM ABI faster. A discount function that reads
cart.totalPrice and ignores everything else processes a 12 KB cart
in ~200 bytes of JSON.
- Smaller WASM payloads. Functions doing simple work don’t have to
scan past 80% of the input. Each unneeded field costs JSON parse time
in the QuickJS runtime that backs Javy dynamic-mode WASM.
- Explicit dependencies. Reading the manifest makes it obvious which
fields the function actually reads. Future-you will thank present-you.
- Tighter security. Even though your function runs in a sandboxed
WASM module, projecting away fields you don’t need limits the blast
radius of accidental logging or output payloads.
Manifest
inputFields is a sibling field on each entry in extensions.functions:
{
"handle": "smart-discounts",
"name": "Smart Discounts",
"version": "1.0.0",
"extensions": {
"functions": [
{
"type": "discount",
"handle": "vip-discount",
"entrypoint": "dist/vip.wasm",
"inputFields": {
"cart": {
"lines": {
"id": true,
"quantity": true,
"merchandise": {
"id": true,
"sku": true,
"price": true
}
},
"totalPrice": true,
"discountCodes": true
},
"customer": {
"id": true,
"tags": true
}
}
}
]
}
}
Semantics
The tree mirrors the runtime input shape for your function type (see
each function’s reference page). Leaves describe whether to keep a key;
nested objects recurse.
| Leaf value | Meaning |
|---|
true | Include the field at this position. If it’s an array or object, copy the entire subtree. |
{ ... } (plain object) | Recurse — include only the listed sub-fields of this value. |
false | Silently drop the key (same as omitting). Useful when generating manifests programmatically. |
| omitted key | Drop the field entirely. |
inputFields missing or {} | Full input is passed (backward compatible). |
null / undefined | Same as missing — no projection requested. |
Arrays project per-item
"lines": {
"id": true,
"quantity": true,
"merchandise": { "sku": true }
}
Each element of cart.lines is projected to
{ id, quantity, merchandise: { sku } }. Nested objects inside array
items recurse the same way. The projector walks every element, so:
- An empty array stays an empty array.
- A 200-item array projects 200 times — projection itself is O(n) over
the array length.
Scalars at a sub-projection position
If your map declares cart: { totalPrice: { ... } } but totalPrice is
actually a number, the projector passes it through unchanged (it
can’t recurse into a scalar). This is forgiving by design: apps that
probe an optional object that some shops have flattened to a scalar
won’t break.
Missing keys
A key that’s in your inputFields but missing on the runtime input is
omitted from the output (no undefined written). Reading
input.customer?.tags in your function code stays the right pattern.
Examples per function type
Each function type has its own input contract — these examples show
typical projections.
Discount (only reads totals + codes)
{
"type": "discount",
"handle": "vip",
"inputFields": {
"cart": {
"totalPrice": true,
"currency": true,
"discountCodes": true,
"lines": { "id": true, "quantity": true, "price": true }
},
"customer": { "id": true, "tags": true }
}
}
Shipping rate (reads cart, destination)
{
"type": "shipping_rate",
"handle": "zone-rates",
"inputFields": {
"cart": {
"totalPrice": true,
"itemCount": true,
"currency": true,
"lines": { "quantity": true, "merchandise": { "id": true } }
},
"destination": {
"country": true,
"province": true,
"zip": true
},
"currency": true
}
}
Payment customization (reads payment methods, cart total)
{
"type": "payment_customization",
"handle": "hide-cod-over-1000",
"inputFields": {
"cart": { "totalPrice": true, "currency": true },
"paymentMethods": true
}
}
{
"type": "cart_transform",
"handle": "bundle-pricing",
"inputFields": {
"cart": {
"lines": {
"id": true,
"quantity": true,
"price": true,
"originalPrice": true,
"properties": true,
"merchandise": {
"id": true,
"sku": true,
"metafields": true
}
}
}
}
}
Delivery customization (reads cart, options)
{
"type": "delivery_customization",
"handle": "rename-zones",
"inputFields": {
"cart": { "totalPrice": true, "currency": true },
"deliveryOptions": true
}
}
Order validation (reads cart, customer, address)
{
"type": "order_validation",
"handle": "max-qty-guard",
"inputFields": {
"cart": {
"lines": { "id": true, "quantity": true, "merchandise": { "sku": true } },
"totalPrice": true
},
"customer": { "id": true, "email": true },
"shippingAddress": { "country": true, "zip": true },
"step": true
}
}
Fulfillment constraints (reads lines + attributes + location catalogue)
{
"type": "fulfillment_constraints",
"handle": "hazmat-routing",
"inputFields": {
"cart": {
"lines": {
"id": true,
"quantity": true,
"merchandise": {
"sku": true,
"attributes": true,
"metafields": true
}
}
},
"fulfillmentLocations": true,
"shippingAddress": { "country": true, "province": true }
}
}
Local / pickup point options (reads cart + address)
{
"type": "local_pickup_options",
"handle": "store-pickups",
"inputFields": {
"cart": {
"totalPrice": true,
"lines": { "quantity": true, "merchandise": { "id": true } }
},
"shippingAddress": { "country": true, "province": true, "city": true, "zip": true },
"currency": true
}
}
Validation rules
The install endpoint validates the inputFields tree before persisting
the function. Errors are returned as HTTP 400 with a path-qualified
message so you know which key is wrong:
inputFields.cart.lines must be true/false or a nested object (got string)
Validation walks the tree recursively and applies these rules:
| Rule | Failure example | Error message |
|---|
| The root must be a plain object (or omitted). | "inputFields": "all" | inputFields must be a plain object, got string |
Every leaf must be true, false, or a nested plain object. | "id": 1 | inputFields.cart.lines.id must be true/false or a nested object (got number) |
| Arrays are not allowed as values. | "lines": ["id"] | inputFields.cart.lines must be a plain object, got array |
null is not allowed as a value. | "customer": null | inputFields.customer must be true/false or a nested object (got null) |
Validation is path-tracking, so deeply-nested errors report their full
dotted path. Update the manifest and reinstall.
Backward compatibility
Manifests without inputFields (or with an empty {}) keep working
unchanged — they receive the full input. There is no version flag to
set. You can add inputFields to an existing manifest without changing
your function’s behaviour, as long as the projection includes every
field your function reads.
If you ship a new function version that reads a new field but forget to
add it to inputFields, the field arrives as undefined to your
function. Bump the projection at the same time as the field-read.
The flow inside the backend (in apps/functions/app-functions.service.ts)
when dispatching a function is:
// 1. Look up active functions of this type for the store.
const fns = await this.getActiveFunctions(storeId, type);
// 2. Per function, optionally project the input down to its subtree.
const results = await Promise.all(
fns.map(async (fn) => {
const projected = fn.inputFields
? projectByFieldMap(inputData, fn.inputFields)
: inputData;
// 3. Serialize, hand to the WASM worker, parse the result.
return await this.wasmExecutor.execute(fn, projected);
}),
);
Two important properties:
- Per-function projection. Each function gets its own projected
input — different apps with different
inputFields see different
trees of the same dispatch.
- Empty / missing projection is a no-op. The projector returns the
input unchanged. Manifests without
inputFields cost zero.
The projector itself (projectByFieldMap) lives in
apps/functions/input-field-projection.util.ts and is covered by
input-field-projection.util.spec.ts in the backend tests. The
validator (validateFieldMap in the same file) is the same one the
install endpoint calls — install-time validation and dispatch-time
projection share their understanding of what’s valid.
Concrete numbers from typical carts:
| Cart size | No projection | With minimal projection |
|---|
| 1 line | ~3 KB input | ~200 B |
| 10 lines | ~15 KB input | ~600 B |
| 100 lines | ~120 KB input | ~5 KB |
Time saved per dispatch is dominated by JSON serialization on the host
side and JSON parsing on the worker side. On a 100-line cart, a minimal
projection typically shaves 5–15 ms off the dispatch round trip —
worth it on every checkout interaction.
Best practices
- Project to exactly what you read. Don’t over-project “just in
case” — every extra field is a future maintenance question.
- Pin the version. Every time you change which fields your function
reads, bump the function version and update
inputFields in the same
commit.
- Use
true for opaque subtrees. If you don’t know the inner shape
of merchandise.metafields and want everything under it, just write
"metafields": true — the entire subtree is copied as-is.
- Don’t project
cart.lines to a scalar set if you also read
per-line metafields. Metafields live under merchandise.metafields
per line, so recurse into merchandise even if you only read one
metafield path.
See also