Skip to main content
Oxygen Functions are TypeScript handlers that ship inside your SDK app and run server-side on Oxygen’s managed V8 isolate runtime, with direct access to your project’s data plane. Reach for them whenever the browser shouldn’t do the work itself:
  • 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.
No separate backend to stand up — a function is versioned and deployed with your frontend by 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 at functions/<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. 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.
Requires @oxy-hq/sdk 2.10 or later. Before that the status was discarded and every response resolved.
functions/daily-rollup.ts
oxy-app.json
  • req is { method, headers, body } — body is the raw request body as a string, and header names are lower-case. A scheduled or Airway run sees POST with no headers. Response / Response.json(...) are polyfilled by the runtime.
  • ctx is the data plane:
    • ctx.query / ctx.queryStream — read-only, row-capped SQL. ctx.query resolves to { rows, truncated }
    • ctx.semantic.query — a semantic-model query
    • ctx.warehouse.query(db, sql) and ctx.warehouse.{insert, exec, upsert} — reads, and writes allowlisted by destinations
    • ctx.airhouse — append the app’s own facts (see Where data goes)
    • ctx.oltp — read and write the app’s own records
    • ctx.storage — the app’s file storage
    • ctx.airway.run — kick off an ELT pipeline
    • ctx.fetch — outbound HTTP, SSRF-allowlisted, HTTPS-only, size/time-capped
    • ctx.env — per-app secrets; ctx.secrets.set writes them back
    • ctx.email.send — email the app’s users
    • ctx.org.people() — the org’s people directory, read-only
    • ctx.crypto.{hmac, verifyHmac, timingSafeEqual} — synchronous HMAC helpers (SHA-256 and hex by default)
    • ctx.user, ctx.log
Capabilities that reach beyond the app’s own reads are off until the function’s manifest entry declares them — see Capabilities.

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, and ctx.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 in destinations and says why in customerWarehouseWrites:
oxy-app.json
Prefer moving the data instead: facts to ctx.airhouse, records to ctx.oltp.

Capabilities

Each function’s entry in oxy-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 a webhook block:
oxy-app.json
The sender posts to 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 the useFunction hook to invoke a route function imperatively — e.g. from a button:
  • invoke(body?, { idempotencyKey? }) returns the function’s JSON response.
  • rollup.data holds the last successful result; rollup.error the 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