App Functions — Write the Handler
Start from the trigger's default template and its types — fetch both with
salla_functions action=get, app_id, trigger. Copy the template verbatim and write all
your code inside the body. Keep the wrapper's first and last lines exactly as given (full rule
plus local type-check → salla-app-functions-validate).
This skill assumes sync-vs-async is already decided in salla-app-functions-design (a sync
merchant_actions trigger blocks/modifies the operation; an async event is fire-and-forget).
Keep the handler minimal — delegate to your own API
Best practice: the function is a thin transform layer, not your business logic. Do the real
work in your own backend API and use the App Function only to (1) read the trigger payload,
(2) call your API, and (3) transform its response into the Resp the trigger expects. The
sandbox is a constrained V8 isolate with a hard time budget (5 s sync / 30 s async) and no npm —
heavy logic, SDKs, persistence, and long work belong on your server, not in here. So a sync
action (e.g. shipment.creating) should: validate the input → fetch your API → map its result
to the trigger's Resp. Minimal and concise keeps it within budget and easy to type-check
against the trigger's types.
1. Read the payload field names from the fetched types FIRST (strict)
STRICT RULE — applies to writing OR modifying any App Function. Before you write or change any
property access on the payload, fetch every URL in the types array returned by
salla_functions action=get (app_id, trigger) and read the exact field names and shapes
from those .d.ts type definitions. The typed payload your handler reads is
context.payload.data — build every property access against the names the .d.ts actually
declares (and any nested shapes / enums it references), not the names you expect. For a
shipping AWB function, that means reading the real Shipments payload fields from its types
(e.g. the address, parcel, and weight fields as the .d.ts names them) instead of assuming
sender_address / receiver_address / weight. The same types URLs feed the local
type-check in salla-app-functions-validate, which fails the build if a guessed field
doesn't exist — but reading them first is what lets you write the body correctly the first
time.
Gate: "Before writing OR modifying the body, every field the handler reads off
context.payload.data was confirmed against a field actually declared in the (re-)fetched
types .d.ts — none guessed?"
export default async (context: Shipments): Promise<Resp> => {
// ↑ template's first line, unchanged ↓ ALL your code goes inside the body
console.log("Shipment cancelled event triggered");
return Resp.success().setData({});
};
Context object
Illustrative shape — the authoritative types are the trigger's types .d.ts from
salla_functions action=get; type-check against those (the data shape is per-trigger).
context.merchant; // { id: number | string, … }
context.payload; // { event, created_at, merchant, data: { … } } — data varies by trigger
context.settings; // per-merchant App Settings; optional (undefined/null until configured)
Calling the Salla Admin API from inside a function typically needs no token wiring — confirm
the exact auth the runtime injects from the trigger's types / function context, then call
the Admin API directly. Request the minimum OAuth scopes for the endpoints you call
(scope setup and token handling → salla-app-auth); base URL, endpoints, and error shapes
→ salla-api-core. Settings: context.settings?.apiKey (optional chaining — undefined
until configured).
The response contract (mandatory return)
Every handler returns an object conforming to the Response contract
(Responses):
| Field | Type | Required | Meaning |
|---|---|---|---|
success |
boolean |
Yes | Did your logic complete? (sync: drives proceed-vs-reject) |
data |
object |
No | Payload to return / apply to the entity (sync actions) |
error |
string |
No | Human-readable error; required when success:false on a sync action |
status |
number |
No | Optional HTTP status (via the builder); default 200 |
message |
string |
No | Optional informative message (via the builder) |
Two return styles — both valid:
// 1. Plain object — simplest, lowest overhead (good for sync actions, simple events)
return { success: true, data: {} }; // data is optional; include it (even {}) on sync actions
// 2. Response utility class — recommended for Customer Events and complex Merchant Events
return Resp.success().setData({ order_id: id }); // .setData({}) is the recommended convention
return Resp.error().setMessage("Something went wrong").setStatus(500);
Builder methods (chainable): Resp.success() / Resp.error() start a response; .setData(obj)
is optional but recommended — pass {} even when empty, and especially on sync actions, where
the returned data is merged/applied to the entity; .setStatus(code) is optional (default
200); .setMessage(msg) is optional. Resp is a pre-declared runtime global — use it
directly, no import.
Entity-named builders. Some triggers expose an entity-specific builder instead of the
generic Resp — same .success() / .error() shape plus entity setters. shipment.creating
requires the Shipment class (context type Shipments, return type Promise<Shipment>):
return Shipment.success().setShipmentNumber(id) (always call .setShipmentNumber() on
success), return Shipment.error().setMessage("…"). Confirm the exact builder + setters from
the trigger's
types (see salla-app-functions-validate) before relying on them.
Sync actions: what the return does
For a sync action (e.g. shipment.creating) the merchant is blocked waiting on you:
success: true→ the action proceeds, and yourdatais merged/applied to the resulting entity.success: false→ the action is rejected and yourerror/.setMessage()is shown to the merchant immediately. Make it clear and actionable.
For an async event the return is informational only — logged, never affecting the already-completed operation. Still always return a response on every path.
Sandbox constraints (V8 isolate — NOT full Node.js)
App Functions run in a V8 isolate, not a Node.js runtime (Node.js Support). Native Node APIs are limited:
- No external packages — npm / external libraries are not importable (planned for a future version). Only pre-declared globals and web-standard APIs.
- Unsupported Node core modules (using them errors):
fs,fs/promises,net,tls,dgram,http/httpsservers,child_process,cluster,worker_threads,repl,readline,zlib. - Partially supported:
crypto= Web Crypto API only (globalThis.crypto);buffer= basic;stream= WHATWG streams only;os/path/util/events= partial. - Outbound HTTP → the standard
fetch()API (nohttp/httpsclient modules, no servers). This is the only supported way to reach external or Salla APIs.
Bound every external fetch to the timeout
Bound each fetch with an AbortController so one slow upstream can't blow the budget. Keep
each call well under the trigger's budget — a sync action targets < 500 ms total, an async
event has up to 30 s (confirm the exact limits → salla-app-functions-design). Validate and
shape the response before applying it: extract the specific fields you need and return only
those, so a malformed or hostile upstream body never flows into the entity.
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 2000); // < 2s per call in a sync action
try {
const res = await fetch("https://api.example.com/data", {
signal: controller.signal,
});
if (!res.ok) return Resp.error().setMessage("Upstream error").setStatus(502);
const body = await res.json();
// Validate the shape and extract only the fields you trust — don't return raw JSON.
if (typeof body?.tracking_number !== "string") {
return Resp.error().setMessage("Invalid upstream response").setStatus(502);
}
return Resp.success().setData({ tracking_number: body.tracking_number });
} catch (error) {
return Resp.error().setMessage("Upstream timeout").setStatus(504);
} finally {
clearTimeout(timeout);
}
Outbound-call safety:
- Validate before applying — check
res.ok, then validate the body's shape and return only the fields you need (above), so an error body or extra fields never reach the entity. - Trust your own endpoints — call hard-coded URLs; when a URL comes from settings, match
it against an allowlist of hosts you control before the
fetch. - Sanitize merchant input —
context.settingsis merchant-controlled. Validate type and format, and encode/escape each value for its target (path/query encoding, header value) before placing it in an outbound request. - Keep secrets out of logs — never log
context.settingsor tokens.
Gate: "Returns Resp.success().setData(...) on every path, no npm/unsupported core, every
fetch bounded, response validated/shaped before use, settings sanitized, no secrets logged?"
→ salla-app-functions-validate.