Examplary
Start for free
Developer docs

Hooks

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, Math and RegExp
  • fetch(), with Headers, Response and URLSearchParams
  • TextEncoder
  • crypto.randomUUID(), and crypto.subtle.digest() with SHA-1, SHA-256, SHA-384 or SHA-512
  • console, although its output isn't available to you yet

Not available:

  • Node.js modules, like fs, http, crypto or Buffer, and npm packages that need them
  • Timers like setTimeout
  • AbortController, streaming response bodies, Blob and FormData

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.

Keep hooks stateless

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_KEY

Then 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;
};
Read secrets inside hooks

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