---
title: "Writing a function"
description: "The rayfin/functions project layout, registering functions with udf.func in function_app.ts, typed data access, and the auto-generated types.ts schema."
url: https://rayfin.ai/docs/functions/writing-functions
markdown_url: https://rayfin.ai/docs/functions/writing-functions.md
section: functions
product: Rayfin
sdk_version: 1.34.0
cli_version: 1.33.2
last_updated: 2026-08-23T15:47:11-07:00
source: functions/writing-functions.mdx
---

# Writing a function

> The rayfin/functions project layout, registering functions with udf.func in function_app.ts, typed data access, and the auto-generated types.ts schema.

This page covers the `rayfin/functions/` project itself — its layout, how to register a
function, how to read and write your entities from inside one, and how its generated types
work.

> [!WARNING]
> Functions are experimental and are not available in every Fabric region or tenant. See
> [Functions](/docs/functions) before you depend on them.

## Project layout [#project-layout]

After `npx 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 — never hand-edit
    tsconfig.json        ← references: [{ "path": ".." }]
    package.json
    host.json
    local.settings.json
```

`rayfin/functions/tsconfig.json` uses `composite: true` with a project reference to
`rayfin/`, so functions can `import type` from your data entities without duplicating type
definitions.

## Registering a function [#registering-a-function]

Every function is registered with `udf.func(name, handler, [])` from
`@microsoft/fabric-user-data-functions`:

```typescript title="rayfin/functions/src/function_app.ts"
import { UserDataFunctions } from '@microsoft/fabric-user-data-functions';

const udf = new UserDataFunctions();

/**
 * A simple greeting function.
 *
 * The input/output types for this function are autogenerated in ./types.ts so that
 * RayfinClient can invoke it with full type-safety from your frontend app.
 */
udf.func('helloWorld', (firstName: string, lastName: string): string => {
  console.log(`helloWorld invoked for ${firstName} ${lastName}`);
  return `Hello ${firstName} ${lastName}!`;
}, []);
```

* The first argument is the function name — it must match the key typegen produces in
  `AppFunctionsSchema`.
* The second argument is the handler: typed parameters plus a return type, both extracted by
  typegen into `types.ts`.
* The third argument is reserved for future middleware — pass an empty array, `[]`.
* Always register functions with `udf.func(...)`. Don't export bare functions instead —
  typegen only sees registrations made through `udf.func`.

## Typed data access with `RayfinContext` [#typed-data-access-with-rayfincontext]

Add a `RayfinContext` parameter to reach the same data client `client.data.<Entity>` uses on
the frontend — `.select().where().execute()`, the same chain throughout:

```typescript title="rayfin/functions/src/function_app.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();
  return data.Entry.select(['id', 'message', 'createdAt']).execute();
}, []);

udf.func('addEntry', async (
  message: string,
  ctx: RayfinContext<AppSchema>
): Promise<void> => {
  console.log('addEntry invoked');
  const data = ctx.getDataClient();
  await data.Entry.create({ message });
}, []);
```

Pass your `AppSchema` type — the same schema type you already pass to `RayfinClient<AppSchema>`
on the frontend — as the generic parameter, so `getDataClient()` only allows valid entity
names and fields. A bare `RayfinContext` (no generic) still works, but `getDataClient()`
returns untyped (`Record<string, any>`) access instead.

`RayfinContext` is a runtime-injected parameter, so typegen automatically strips it from the
generated input type — `addEntry`'s generated input is `{ message: string }`, not
`{ message: string; ctx: RayfinContext }`.

`RayfinContext` API:

| Member                       | Description                                                                                          |
| ---------------------------- | ---------------------------------------------------------------------------------------------------- |
| `ctx.getDataClient()`        | Returns the entity data client — typed when using `RayfinContext<AppSchema>`.                        |
| `ctx.baseUrl`                | The Rayfin endpoint URL (readonly).                                                                  |
| `ctx.accessToken`            | The auth token for the current request (readonly).                                                   |
| `ctx.publishableKey`         | The Rayfin publishable key (readonly).                                                               |
| `ctx.getSecret(name)`        | Returns a secret — see below.                                                                        |
| `ctx.getToken(audienceType)` | Returns a delegated token for an external resource — see [Connections](/docs/functions/connections). |

* Import `RayfinContext` from `@microsoft/fabric-user-data-functions` — **not** from
  `@microsoft/rayfin-functions`.
* Use `console.log(...)` / `console.error(...)` for logging, not `ctx.log`.
* Import data entity classes with `import type`, not a runtime `import` — a runtime import of
  `@microsoft/rayfin-core` decorators would pull unnecessary dependencies into the functions
  bundle. Use the `.js` extension on relative imports, matching ESM resolution:

```typescript
import type { TodoItem } from '../../data/TodoItem.js';
```

This works because `rayfin/functions/tsconfig.json` has `"references": [{ "path": ".." }]`
pointing at `rayfin/tsconfig.json`.

## Function secrets [#function-secrets]

Use `ctx.getSecret(name)` when a function needs a secret value. The runtime checks
host-provided invocation secrets first, then falls back to `process.env[name]`:

```typescript title="rayfin/functions/src/function_app.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;
}, []);
```

Set project secrets with `rayfin secret set <NAME>`.

## Generated types [#generated-types]

The CLI parses every `udf.func()` call under `rayfin/functions/src/` and generates
`types.ts`:

```typescript title="rayfin/functions/src/types.ts"
export type AppFunctionsSchema = {
  helloWorld: {
    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`.
* Never hand-edit `types.ts` — it's regenerated from your `udf.func()` calls and any manual
  changes are overwritten.
* `Promise<T>` return types are unwrapped to `T` in the generated schema.

See [Calling functions from your app](/docs/functions/calling-functions) for importing
`AppFunctionsSchema` on the frontend.

```prompt title="Write a Rayfin function with typed data access"
In my Rayfin project, add a new function to rayfin/functions/src/function_app.ts (running
`npx rayfin functions init` first if that directory doesn't exist yet).

Register it with udf.func('<name>', handler, []) and give the handler a
RayfinContext<AppSchema> parameter so ctx.getDataClient() is fully typed against my entities
— use the same AppSchema type my frontend already passes to RayfinClient<AppSchema>. Import
RayfinContext from @microsoft/fabric-user-data-functions, not @microsoft/rayfin-functions,
and use import type for any data entity classes.

After adding it, confirm it appears in the regenerated rayfin/functions/src/types.ts — never
hand-edit that file — then deploy with `npx rayfin up` (or `npx rayfin up functions deploy`)
and tell me how to call it from the frontend.
```
