Skip to main content

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 valueMeaning
trueInclude 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.
falseSilently drop the key (same as omitting). Useful when generating manifests programmatically.
omitted keyDrop the field entirely.
inputFields missing or {}Full input is passed (backward compatible).
null / undefinedSame 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
  }
}

Cart transform (reads lines + attributes)

{
  "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:
RuleFailure exampleError 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": 1inputFields.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": nullinputFields.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.

How the platform consults the projection at dispatch time

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:
  1. Per-function projection. Each function gets its own projected input — different apps with different inputFields see different trees of the same dispatch.
  2. 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.

Performance impact

Concrete numbers from typical carts:
Cart sizeNo projectionWith 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