- a parameterized or privileged warehouse query,
- a warehouse write (insert / upsert / exec),
- kicking off an Airway ELT run,
- an outbound API call to a third-party service.
oxyc publish.
An Oxygen Function is a bundled TypeScript handler — not a YAML
Automation task. It runs with
the app’s project-level access, not the invoking visitor’s — with one exception,
ctx.warehouse writes to a managed warehouse.Authoring a function
A function is a file atfunctions/<name>.ts that exports a default
async (req, ctx) => Response, declared in the app’s oxy-app.json:
The status you return is the status the caller sees. Requires
useFunction’s
invoke resolves on a 2xx and rejects on anything else, with a
FunctionError carrying status, the parsed body, and any logs the run
captured — so return Response.json({ error: "…" }, { status: 409 }) is a
rejection the caller can branch on, not a value it has to inspect.@oxy-hq/sdk 2.10 or later. Before that the status was discarded and
every response resolved.functions/daily-rollup.ts
oxy-app.json
reqis{ method, headers, body }—bodyis the raw request body as a string, and header names are lower-case. A scheduled or Airway run seesPOSTwith no headers.Response/Response.json(...)are polyfilled by the runtime.ctxis the data plane:ctx.query/ctx.queryStream— read-only, row-capped SQL.ctx.queryresolves to{ rows, truncated }ctx.semantic.query— a semantic-model queryctx.warehouse.query(db, sql)andctx.warehouse.{insert, exec, upsert}— reads, and writes allowlisted bydestinationsctx.airhouse— append the app’s own facts (see Where data goes)ctx.oltp— read and write the app’s own recordsctx.storage— the app’s file storagectx.airway.run— kick off an ELT pipelinectx.fetch— outbound HTTP, SSRF-allowlisted, HTTPS-only, size/time-cappedctx.env— per-app secrets;ctx.secrets.setwrites them backctx.email.send— email the app’s usersctx.org.people()— the org’s people directory, read-onlyctx.crypto.{hmac, verifyHmac, timingSafeEqual}— synchronous HMAC helpers (SHA-256 and hex by default)ctx.user,ctx.log
Where data goes
Pick the store by the shape of the data:<slug> is the app’s slug with - replaced by _ (store-ops →
app_store_ops). The schema is always derived from the slug — a manifest can’t
name another app’s schema. Tables are created by the SQL files in the declared
migrations directory when the app is published. ctx.airhouse and ctx.oltp
write as the app, whoever invoked the function, so a scheduled run writes
the same way a click does.
Airhouse tables take no primary keys, unique constraints, or indexes, so give
every fact the id its source assigned plus a recorded_at, and record
corrections as new facts rather than updates. For records, always use bound
parameters ($1, $2), and put statements that must land together in one
ctx.oltp.tx.
functions/complete-checklist.ts
Who a write runs as
Reads always run with the app’s project-level access, andctx.airhouse /
ctx.oltp always write as the app. ctx.warehouse writes to an
Oxygen-managed warehouse are the one place the invoker matters: the
connection is opened with a short-lived credential whose role follows the
workspace role of the person who called the function.
A denied write surfaces as a permission error on the statement (
Reader role cannot execute Update statements) while queries in the same function keep
working — that combination means the caller’s role, not a broken
destinations allowlist.
Customer warehouses are read-only
Databases you configure with your own credentials (BigQuery, Snowflake, ClickHouse, …) are read-only to functions. A write to one is refused unless the function lists the database indestinations and says why in
customerWarehouseWrites:
oxy-app.json
ctx.airhouse, records to ctx.oltp.
Capabilities
Each function’s entry inoxy-app.json declares what it may do. Every
capability is fail-closed — omit it and the call is rejected before any
connection is opened.
Other per-function keys:
route, schedule and timezone, airwayStep,
webhook (see Webhooks), timeoutSeconds, cache, and
retries — { maxAttempts, minTimeoutMs, maxTimeoutMs } for scheduled and
background runs (route calls are never retried).
oxyc publish refuses a function that calls a ctx capability its manifest
entry doesn’t declare.
Invocation modes
A function can be reached three ways, each with its own identity and default timeout (a ceiling; max 300s):
The route endpoint (
POST /customer-apps/<org>/<slug>/fn/<name>) is POST-only
(405 otherwise), rate-limited per (user, app, function), and every invocation
is audited.
Webhooks
A function can receive an unauthenticated POST from a third party — a payment provider, a delivery platform — by declaring awebhook block:
oxy-app.json
POST /api/webhooks/apps/<org>/<app>/<name>. Oxygen
verifies every request — an HMAC-SHA256 of the raw body, keyed by the app
secret named in secretVar, compared in constant time against
signatureHeader — before the function is queued, so your code never sees an
unverified request. secretVar may hold several comma-separated keys, so a
provider’s key rotation doesn’t drop events. A verified request is answered
202 immediately and the function runs in the background with no invoking
user. Without a webhook block the endpoint answers 404.
Calling a route function from React
Use theuseFunction hook to invoke a route function imperatively — e.g. from
a button:
invoke(body?, { idempotencyKey? })returns the function’s JSON response.rollup.dataholds the last successful result;rollup.errorthe last error.- Concurrent identical invokes are deduplicated into one request.
Caching
Results are not cached by default — functions are often side-effectful. A read-only function can opt into a server result cache by adding"cache": { "ttlSeconds": N } to its manifest entry; the cache is keyed per
(build, user, body), invalidated by the next publish, and bypassed with
?refresh.
Next steps
Hooks & components
The rest of the SDK surface
SDK overview
Scaffold, wire up, and deploy an app