rayfin-functions
$
npx mdskill add microsoft/aitour27-BRK390-building-ai-applications-with-microsoft-databases-and-fabric/rayfin-functionsWrite server-side user-defined functions (UDFs) that run in the Fabric runtime and are invocable from your frontend via `RayfinClient`.
SKILL.md
.github/skills/rayfin-functionsView on GitHub ↗
---
name: rayfin-functions
description: "Use when writing Rayfin user-defined functions (UDFs), invoking functions from frontend code, or using functions CLI commands. Triggers: rayfin functions, udf.func, UserDataFunctions, RayfinContext, FabricContext, AppFunctionsSchema, functions init, dev functions apply, client.functions, invoke, func start, function_app.ts, types.ts, rayfin/functions/, FunctionClient, FunctionsSchema, fabric-user-data-functions, functionsBaseUrl, VITE_RAYFIN_FUNCTIONS_URL, bootstrap.ts, rayfinClient.ts"
metadata:
author: microsoft
version: 0.1.0
rayfin-managed: true
---
# Rayfin Functions
Write server-side user-defined functions (UDFs) that run in the Fabric runtime and are invocable from your frontend via `RayfinClient`.
## When to Use Functions
Reach for functions whenever logic should run on the backend rather than the frontend. Use them for sensitive operations (secrets, API keys, privileged data access), business logic that must not be exposed or tampered with in client code, server-side validation, and anything that aggregates or transforms data before returning it to the client. If a frontend feature needs trusted server-side behavior, implement it as a function.
## Project Structure
After `rayfin functions init`, the functions project lives at `rayfin/functions/`:
```text
rayfin/
data/ ← entity classes (shared via TS project references)
functions/
src/
function_app.ts ← register functions here
types.ts ← auto-generated by typegen
tsconfig.json ← references: [{ "path": ".." }]
package.json
host.json
local.settings.json
```
`tsconfig.json` uses `composite: true` with a project reference to `rayfin/` so functions can `import type` from data entities without duplicating definitions.
## Writing Functions
### Simple function (no context)
```ts
import { UserDataFunctions } from '@microsoft/fabric-user-data-functions';
const udf = new UserDataFunctions();
udf.func('greet', (firstName: string, lastName: string): string => {
return `Hello ${firstName} ${lastName}!`;
}, []);
```
- First argument: function name (must match the key in `AppFunctionsSchema`).
- Second argument: handler function with typed parameters and return type.
- Third argument: empty array `[]` (reserved for future middleware).
- Parameters and return type are extracted by typegen into `types.ts`.
### Function with RayfinContext
Use `RayfinContext` to access the GraphQL-backed data client, auth tokens, and logging:
```ts
import { UserDataFunctions, type RayfinContext } from '@microsoft/fabric-user-data-functions';
type AppSchema = {
Entry: { id: string; message: string; createdAt: string };
};
const udf = new UserDataFunctions();
udf.func('getEntries', async (ctx: RayfinContext<AppSchema>): Promise<{ id: string; message: string }[]> => {
console.log('getEntries invoked');
const data = ctx.getDataClient();
const entries = await data.Entry.select(['id', 'message', 'createdAt']).execute();
return entries;
}, []);
```
**`RayfinContext` API:**
- `ctx.getDataClient()` — returns an entity data client (type-safe when `RayfinContext<AppSchema>` is used). Query with `.select([...]).where(...).execute()`, same chain as `client.data.<Entity>` in frontend code.
- Use `console.log(...)` / `console.error(...)` for logging (not `ctx.log`).
- `ctx.baseUrl` — Rayfin endpoint URL (readonly).
- `ctx.accessToken` — auth token for the current request (readonly).
- `ctx.publishableKey` — Rayfin publishable key (readonly).
- `ctx.getSecret(name)` — returns a secret from the host-provided secret bag first, and falls back to `process.env[name]` when the secret is not present in the bag.
**Type-safe context with `RayfinContext<AppSchema>`:**
Pass your `AppSchema` type (the same schema used with `RayfinClient<AppSchema>`) as a generic to `RayfinContext` for full type safety on `ctx.getDataClient()`:
- `ctx.getDataClient()` returns typed entity accessors — e.g. `data.Todo.select(['id', 'title'])` only allows valid fields.
- Without a generic (`RayfinContext` alone), `getDataClient()` still works but entity access is untyped (`Record<string, any>`).
- Import `RayfinContext` from `@microsoft/fabric-user-data-functions` (not from `@microsoft/rayfin-functions`).
- `RayfinContext` (with or without a generic) is a runtime-injected parameter — typegen automatically excludes it from the generated schema input type.
- Use `.js` extension on relative imports (ESM resolution).
### Function with mixed parameters
```ts
udf.func('addEntry', async (message: string, ctx: RayfinContext<AppSchema>): Promise<void> => {
console.log('addEntry invoked');
const data = ctx.getDataClient();
await data.Entry.create({ message });
}, []);
```
Runtime-injected types (`RayfinContext`, `FabricContext`, `FabricItem`, `DataConnection`, `FabricSqlConnection`, `FabricConnection`, `FabricUdf`, `FabricLogger`) are stripped from the schema — only `message: string` appears in the generated `input` type.
### Function secrets via RayfinContext
Use `ctx.getSecret(name)` when the function needs a secret value.
The runtime checks host-provided invocation secrets first.
If a secret is missing there, it checks `process.env[name]` as a fallback.
Set project secrets with `rayfin secret set <NAME>`.
For local function debugging, add the same key in `rayfin/functions/local.settings.json` under `Values` so it flows into `process.env` and is available to `ctx.getSecret(name)`.
```ts
udf.func('readApiKey', async (ctx: RayfinContext): Promise<string> => {
const apiKey = ctx.getSecret('THIRD_PARTY_API_KEY');
if (!apiKey) {
throw new Error('Missing THIRD_PARTY_API_KEY secret.');
}
return apiKey;
}, []);
```
## Using Connections for External Resources
Functions access external resources via **delegated authentication**. The runtime exchanges the user's identity token for a resource-scoped OBO token — your function accesses resources **as the calling user**. You declare a connection, and the runtime provides the scoped token at invocation time.
### Pattern
```ts
import { AudienceType } from '@microsoft/fabric-user-data-functions';
udf.func(
'myFunction',
async (ctx: RayfinContext, /* user params */): Promise<T> => {
const token = ctx.getToken(AudienceType.X);
// Use token with the appropriate SDK client
},
[udf.connection({ audienceType: AudienceType.X })],
);
```
For Azure SDK clients that expect a `TokenCredential`, wrap the token:
```ts
import type { TokenCredential, AccessToken } from '@azure/identity';
class ContextTokenCredential implements TokenCredential {
constructor(private readonly token: string) {}
async getToken(): Promise<AccessToken> {
return { token: this.token, expiresOnTimestamp: Date.now() + 3600_000 };
}
}
```
### Available `AudienceType` values
| AudienceType | Resource | Ask the user for |
|---|---|---|
| `Storage` | OneLake (DFS), Azure Blob/Table/Queue | File URL from Lakehouse properties, or storage account URL |
| `Sql` | Fabric Lakehouse/Warehouse/SQL DB/Mirrored DB, Azure SQL | SQL analytics endpoint or connection string from Fabric Portal |
| `KeyVault` | Azure Key Vault | Vault URL (e.g. `https://my-vault.vault.azure.net/`) |
| `CosmosDB` | Azure Cosmos DB | Account endpoint + database/container names |
| `EventGrid` | Azure Event Grid | Topic endpoint |
| `Kusto` | Azure Data Explorer (Kusto) | Cluster query URL (e.g. `https://<cluster>.<region>.kusto.windows.net`) |
| `PowerBI` | Power BI APIs | _(scopes TBD — check with platform team)_ |
### SQL connections: shared requirements
All Fabric SQL resources (Lakehouse SQL analytics, Warehouse, SQL Database, Mirrored DB) use the same pattern:
- **Package:** `mssql@^12.6.0` (pulls tedious >= 19.2.2). Older tedious (<= 19.1.2) has a LOGIN7 FeatureExt bug causing "socket hang up" on Fabric endpoints.
- **Encryption:** `encrypt: true` (NOT `'strict'`). Matches ODBC `Encrypt=yes`.
- **Auth:** `azure-active-directory-access-token` with `ctx.getToken(AudienceType.Sql)`
| Resource | What to ask for | Database value |
|---|---|---|
| **Lakehouse (SQL analytics)** | SQL analytics endpoint + item GUID (Portal → Settings) | Item GUID (Initial Catalog — **required**) |
| **Warehouse** | SQL endpoint + item GUID (Portal → Settings) | Item GUID |
| **SQL Database** | Full connection string (Portal → Connection strings) | Database name from connection string |
| **Mirrored Database** | SQL analytics endpoint + item GUID (Portal → Settings) | Item GUID |
**Important:** For Lakehouse/Warehouse/Mirrored DB, you MUST pass the item GUID as `database`. Without it, multi-item workspaces cannot route correctly.
```ts
import sql from 'mssql';
// Always ask the user for these values — never invent them
const SQL_SERVER = '<endpoint>.datawarehouse.fabric.microsoft.com';
const DATABASE = '<item-guid-or-db-name>';
udf.func(
'queryData',
async (ctx: RayfinContext, query: string): Promise<Record<string, unknown>[]> => {
const token = ctx.getToken(AudienceType.Sql);
const pool = await sql.connect({
server: SQL_SERVER,
database: DATABASE,
options: { encrypt: true, trustServerCertificate: false },
authentication: { type: 'azure-active-directory-access-token', options: { token } },
});
const result = await pool.request().query(query);
await pool.close();
return result.recordset;
},
[udf.connection({ audienceType: AudienceType.Sql })],
);
```
### OneLake Files (DFS)
Ask the user for the file URL: Fabric Portal → Lakehouse → file → Properties → URL.
Format: `https://onelake.dfs.fabric.microsoft.com/<workspaceId>/<itemId>/Files/<path>`
```ts
udf.func(
'readFile',
async (ctx: RayfinContext, fileUrl: string): Promise<string> => {
const token = ctx.getToken(AudienceType.Storage);
const res = await fetch(fileUrl, { headers: { Authorization: `Bearer ${token}` } });
if (!res.ok) throw new Error(`OneLake read failed: ${res.status}`);
return await res.text();
},
[udf.connection({ audienceType: AudienceType.Storage })],
);
```
### Azure Key Vault
Ask the user for the vault URL (e.g. `https://my-vault.vault.azure.net/`). Install: `@azure/keyvault-secrets`, `@azure/identity`.
```ts
import { SecretClient } from '@azure/keyvault-secrets';
udf.func(
'getSecret',
async (ctx: RayfinContext, kvUrl: string, secretName: string): Promise<string> => {
const credential = new ContextTokenCredential(ctx.getToken(AudienceType.KeyVault));
const client = new SecretClient(kvUrl, credential);
const secret = await client.getSecret(secretName);
return secret.value ?? '';
},
[udf.connection({ audienceType: AudienceType.KeyVault })],
);
```
### Other resources (Cosmos DB, Blob Storage)
All follow the same `ContextTokenCredential` pattern — ask the user for the resource endpoint, wrap `ctx.getToken(AudienceType.X)`, and pass to the relevant Azure SDK client:
- **Cosmos DB** (`AudienceType.CosmosDB`): `new CosmosClient({ endpoint, aadCredentials: credential })` — install `@azure/cosmos`
- **Blob Storage** (`AudienceType.Storage`): `new BlobServiceClient(url, credential)` — install `@azure/storage-blob`
### Connection rules
- **Always ask the user for real endpoint URLs** — never invent them or use `process.env`
- Declare connections in the third argument to `udf.func()`
- Use `ctx.getToken(AudienceType.X)` — never acquire tokens manually
- Install SDK packages in `rayfin/functions/package.json`, not the root
- For SQL: use `mssql@^12.6.0` with `encrypt: true`
### Local development with delegated tokens
When `rayfin dev functions apply` is run and the host is started, the host uses `InteractiveBrowserCredential` to acquire tokens for all audiences for local debugging.
### Importing data entities
Use TypeScript project references to share entity types between data models and functions:
```ts
import type { TodoItem } from '../../data/TodoItem.js';
```
- Use `import type` — entity classes are only needed for their type shape, not runtime value.
- The `../../data/` path works because `tsconfig.json` has `"references": [{ "path": ".." }]` pointing to `rayfin/tsconfig.json`.
- Always use `.js` extension for ESM imports.
## Typegen
The Rayfin CLI parses all `udf.func()` calls in `rayfin/functions/src/` and generates `types.ts`:
```ts
export type AppFunctionsSchema = {
greet: {
input: { firstName: string; lastName: string };
output: string;
};
getEntries: {
input: void; // RayfinContext was stripped
output: Entry[];
};
};
```
- `rayfin functions init` runs typegen once after scaffolding to seed `types.ts`.
- `rayfin dev functions apply` runs typegen once at startup and then keeps a visible watcher running in the same terminal (`[typegen]` prefix) until you Ctrl+C. There is no separate `typegen` CLI command.
- Never hand-edit `types.ts` — it is overwritten by typegen.
## Invoking Functions from Frontend
Import `AppFunctionsSchema` from the functions project and pass it to `RayfinClient`:
```ts
import { RayfinClient } from '@microsoft/rayfin-client';
import { resolveRayfinFunctionsBaseUrl } from '@microsoft/rayfin-local-dev';
import type { AppFunctionsSchema } from '../rayfin/functions/src/types.js';
const client = new RayfinClient<AppSchema, AppFunctionsSchema>({
publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY,
functionsBaseUrl: resolveRayfinFunctionsBaseUrl(),
});
// Type-safe invocation — parameters and return type are checked
const greeting = await client.functions.greet.invoke({ firstName: 'Jane', lastName: 'Doe' });
// For void-input functions (RayfinContext-only)
const entries = await client.functions.getEntries.invoke();
```
- `invoke()` returns `Promise<TOutput>` directly (not an envelope).
- `functionsBaseUrl` is optional — when unset, function invocation uses the deployed Rayfin backend route.
- Bundled Vite templates include same-origin local routing already.
- `rayfin dev functions apply` auto-patches recognized existing client code on first run.
- See [Local Functions Debugging Setup](#local-functions-debugging-setup) if you need to configure a custom project manually.
## Local Functions Debugging Setup
Bare `rayfin dev` automatically:
1. Selects an available Functions port and writes its URL to `VITE_RAYFIN_FUNCTIONS_URL` through `.env.local`.
2. Starts the configured frontend and Functions host together.
3. Routes bundled Vite templates through same-origin `/.rayfin/api/<name>` requests.
4. Returns HTTP 502 if the local Functions host is unavailable instead of invoking deployed function code.
`rayfin dev functions apply` remains available as a compatibility entrypoint and uses the same runtime primitives.
When `VITE_RAYFIN_FUNCTIONS_URL` is unset or the application is built for production, `functionsBaseUrl` is `undefined` and function calls route through the deployed backend.
## Wiring `functionsBaseUrl` for Local Debug
Shipped Vite templates include `@microsoft/rayfin-local-dev`, register `rayfinLocalDev()` in `vite.config.ts`, and resolve `functionsBaseUrl` through the package's browser-safe entrypoint.
For a custom Vite project, add the same plugin before wiring the client:
```ts
import { rayfinLocalDev } from '@microsoft/rayfin-local-dev/vite';
export default defineConfig({
plugins: [rayfinLocalDev()],
});
```
Then pass the same-origin base URL through the existing client configuration:
`src/services/rayfinClient.ts` — accept the new optional config field and pass it to `new RayfinClient(...)`:
```ts
export interface RayfinClientConfig {
baseUrl: string;
publishableKey: string;
localDev: boolean;
/** Local-debugging override. Set when `rayfin dev functions apply` is running. */
functionsBaseUrl?: string;
}
client = new RayfinClient<AppSchema>({
baseUrl: config.baseUrl,
publishableKey: config.publishableKey,
authStorage: true,
functionsBaseUrl: config.functionsBaseUrl,
});
```
`src/services/bootstrap.ts` — pass the env var through, gated on `localDev` so production/Fabric-hosted bootstraps leave it `undefined` and the SDK uses its default routing:
```ts
import { resolveRayfinFunctionsBaseUrl } from '@microsoft/rayfin-local-dev';
const client = initRayfinClient({
baseUrl: apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`,
publishableKey: publishableKey ?? 'local-dev-key',
localDev,
functionsBaseUrl: resolveRayfinFunctionsBaseUrl(),
});
```
The env var `VITE_RAYFIN_FUNCTIONS_URL` is populated automatically while `rayfin dev` runs local Functions.
The CLI writes `RAYFIN_PUBLIC_FUNCTIONS_URL` into `rayfin/.env`, and the framework env projector exposes the Vite-prefixed variant.
For Next.js use `NEXT_PUBLIC_RAYFIN_FUNCTIONS_URL`; for other frameworks read the equivalent public-env-prefixed variable directly during development.
Idempotency: if `functionsBaseUrl` is already present in either file, leave it alone — the user (or a prior agent run) has already done the wiring.
## Rules
- Always register functions with `udf.func(name, handler, [])` — do not export bare functions.
- Use `RayfinContext<AppSchema>` for type-safe data access in functions. Bare `RayfinContext` (without a generic) still works but provides no type safety on `getDataClient()`.
- Keep `types.ts` auto-generated — do not add manual type definitions to it.
- Use `import type` for data entity imports in function files — runtime imports of `@microsoft/rayfin-core` decorators would pull in unnecessary dependencies.
- Functions must return a value or `void` — throwing an error surfaces as an invocation failure with the error message.
- `Promise<T>` return types are unwrapped to `T` in the generated schema.
## Anti-Patterns
- Never import `RayfinContext` from `@microsoft/rayfin-functions` — it comes from `@microsoft/fabric-user-data-functions`.
- Never hand-edit `rayfin/functions/src/types.ts` — it is regenerated by `rayfin functions init` (one-shot) and by the watcher inside `rayfin dev functions apply`.
- Never use `import` (non-type) for data entities in function files — use `import type` to avoid pulling decorator runtime into the functions bundle.
- Never expose a local Functions URL in a production build; leave `functionsBaseUrl` undefined so the SDK uses the deployed route.
- Never make `process.env` your primary production secret source for functions.
- Prefer secrets managed via `rayfin secret set <NAME>` and delegated auth connections via `udf.connection()`.
- Use `process.env` fallback only for local debugging, typically by mirroring keys in `rayfin/functions/local.settings.json`.
## CLI Quick Reference
```bash
# Scaffold
npx rayfin functions init # Scaffold rayfin/functions/, install deps, generate types.ts
npx rayfin functions init --force # Re-scaffold, overwriting existing files
# Local development (also runs the typegen watcher visibly in the same terminal)
npx rayfin dev functions apply # Start local function host against active deployment
npx rayfin dev functions apply --port 7072 # Use a different port
npx rayfin dev functions apply --no-debug # Disable Node.js inspector
npx rayfin dev functions apply --inspect-port 9230 # Custom inspector port
```