The handler context

What a handler is given — its input, the SDK, the caller, its reach, and in a scheduled run the job itself.

A handler receives two arguments, data and ctx. data is the input. ctx is everything the platform knows about this run, and it is built by the platform: nothing in a request can set or override a field of it.

export default async function handler(data, ctx) {
  const { sdk, user, audience, job } = ctx;
  // …
}

data: the input

Run bydata is
An endpointThe request body
An agent or MCP toolThe tool call's arguments
A scheduled jobThe job's payload, the same fixed object on every run
Run live / dry run in the consoleWhat you type in the run panel

Treat it as untrusted input, whoever sends it. Validate what you use, and never build SQL from it except through sdk.sql.

What ctx carries

FieldWhat it is
ctx.sdkThe SDK, scoped to this app and this run. The same object as the destructured sdk.
ctx.userThe caller, in handles: { uid, email, fname, lname, orgUid }. No id, no password fields. null when the caller is not a user.
ctx.reqParamsRoute path parameters: /orders/:orderUid arrives as { orderUid }.
ctx.reqQueryThe parsed query string of the request.
ctx.appConfigThe app's configuration object, or null.
ctx.audienceWhich of the app's organizations this run reaches. See below.
ctx.jobPresent in a scheduled run. See below.
ctx.handlerDelegationSet when another handler reached this one: the action that entered the chain and what it may call. Read-only bookkeeping.

There is no environment. The sandbox exposes no environment variables you set: process.env and Deno.env hold nothing of yours. Configuration belongs in ctx.appConfig; secrets for an outside service belong in the integration that holds them (an AI connection, an email connection, Stripe), which the SDK uses on your behalf.

ctx.user: who is calling

ctx.user.uid is the caller's handle in this app, for anything that records who did something (sdk.storage's ownerUid, a created_by_uid column of your own). ctx.user.orgUid is the organization the call acts in.

In a scheduled run, ctx.user is the job's own principal and the organization it runs as, never the person who created the job.

ctx.audience: which organizations this run reaches

An app's data belongs to the app's organizations. Most runs act inside one of them; a run acting for the app's owner may reach several. ctx.audience says which, so your code can check it rather than infer it:

// { mode: "org" | "all" | "orgs" | "none", orgUids: string[] }
if (ctx.audience?.mode !== "org") {
  // Acting for the app's owner: rows say which org they belong to.
}
modeMeaning
orgOne organization. A caller inside the app, or a job whose org reach is exactly one org, which runs as that org. Rows carry no org column; new rows are that org's.
allEvery organization, including ones created later (orgUids is empty).
orgsThe organizations listed in orgUids.
noneNo organization's data: only the owner's own rows.

In all, orgs and none the run acts for the app's owner, and three things change:

  • every row it reads says which org it belongs to as org_uid (null for the owner's own rows);
  • filters and sdk.aggregate groupings accept org_uid;
  • a new row in an organization's table must name one with org_uid. Without it the write is refused with ROW_ORG_REQUIRED, and an org outside the reach with ROW_ORG_NOT_FOUND. An update never changes a row's org (ROW_FIELD_NOT_ACCEPTED).

ctx.audience is null on the platform's own plane. Which mode a job or agent gets is set by its org reach.

ctx.job: a scheduled run, told about itself

Present when a scheduled job runs the handler, and when the app's owner tries a job-only handler from the console.

FieldWhat it is
cronUidThe job's uid. null in a console try.
runUidThis run's uid. null in a console try.
windowStartWhich firing this is (ISO 8601): the time it was scheduled for, or when a Run now started.
attemptHow many turns of this firing have failed so far; 0 on a first try.
positionWhere a hand-written loop stopped: the value it last passed to next().
each(table, filters, fn, options?)Page through rows, one page per turn, resumable.
next(position)End this turn and carry on from position on the next one.

windowStart is the right ingredient for your own idempotency keys, because every turn and every retry of one firing shares it:

await sdk.withIdempotency({ key: `${ctx.job.windowStart}:${invoice.uid}` }, () =>
  sdk.create("reminders", { invoice_uid: invoice.uid }),
);

How turns, retries and each work is in Jobs that loop.

Next

On this page