Hooks are the functions in your app that run on the server: the settings steps, and the functions you give to a capability, like startExport. You don't call them yourself; Examplary does, when it needs them.
Every hook gets the settings of the installation as its first argument: the workspace's settings, merged with the teacher's own when the app has user settings.
Where hooks run
When you publish your app, the CLI bundles app.ts, with everything it imports, into a single script. Examplary runs that script in a sandbox, a fresh JavaScript environment for every call. That keeps apps apart from each other and from the platform, but it also means hooks can't use everything Node.js offers.
Available in hooks:
- Standard JavaScript, including
async/await,JSON,Date,MathandRegExp fetch(), withHeaders,ResponseandURLSearchParamsTextEncodercrypto.randomUUID(), andcrypto.subtle.digest()withSHA-1,SHA-256,SHA-384orSHA-512console, although its output isn't available to you yet
Not available:
- Node.js modules, like
fs,http,cryptoorBuffer, and npm packages that need them - Timers like
setTimeout AbortController, streaming response bodies,BlobandFormData
Requests made with fetch() have to use HTTPS and can only reach public addresses. A hook can run for up to 25 seconds, with 256 MB of memory. For work that takes longer, like an export that a platform processes in the background, start it in your hook and let Examplary check back later; export options work that way.
Nothing is kept between two calls of a hook. Store what you need to remember in a setting, or return it from your hook when the capability allows it, like the data of an export.
Errors
When a hook throws an error, Examplary shows its message to the person who caused the hook to run: the admin filling in the settings, the teacher exporting a test, or the student opening it. Write those messages for them:
if (res.status === 401) {
throw new Error(
"Your Acme API token has expired. Update it in the app's settings.",
);
}Secrets
Some values belong to your app rather than to a workspace, like the client secret of your OAuth app. Store those as secrets, which only your app's hooks can read:
npx @examplary/cli app set-secret ACME_CLIENT_SECRET "..." --key YOUR_API_KEYThen read them from env inside a hook. Secret names are written in UPPER_SNAKE_CASE, and a secret that isn't set is undefined:
import { env } from "@examplary/apps";
const exchangeCode = async (code: string) => {
const res = await fetch("https://api.acme.com/oauth/token", {
method: "POST",
body: new URLSearchParams({
code,
client_id: "examplary",
client_secret: env.ACME_CLIENT_SECRET!,
}),
});
return (await res.json()).refresh_token;
};Secrets are only available while a hook runs. Read env inside your functions, not at the top of a file, where it would still be empty.
Who a hook runs for
context tells you the workspace a hook runs for, and the user who started it, if any. These are Examplary IDs, which you can use to tell installations apart on your side:
import { context } from "@examplary/apps";
const workspaceId = context.org; // "org_…"
const userId = context.user; // "user_…", or undefined