# Function definition reference

> Exact signatures for defineFunction and defineSchedule, plus the named exports the runtime reads.

A Helix function is a TypeScript file that default-exports `defineFunction` or `defineSchedule` from `@trayai/helix-sdk`. The file's location and suffix determine its trigger and route, and named exports (`input`, `output`) are read by the runtime for validation. This page gives exact signatures, every option with its default, and a summary of the routing rules.

| Helper | Trigger | Location |
|---|---|---|
| `defineFunction` | Incoming HTTP request | Anywhere routable under `functions/` |
| `defineSchedule` | Cron schedule | `functions/_scheduled/` |

App access is set per project on the dashboard's Access Control tab; see [identity and roles](/documentation/guides/identity-and-roles/).

## defineFunction

Handles one HTTP method (or all methods, for files without a method suffix). Generics give compile-time types; the named exports give runtime validation. You write each schema once and use it for both.

```typescript
// @trayai/helix-sdk

// Sentinel type: use instead of `undefined` when skipping input
type NoInput = typeof NoInput;

// No validation, bare handler
function defineFunction(
  handler: (ctx: FunctionContext) => any
): FunctionDef;

// Input only
function defineFunction<TInput extends z.ZodType>(
  handler: (ctx: FunctionContext & { input: z.infer<TInput> }) => any
): FunctionDef;

// Input + output (use NoInput to skip input validation)
function defineFunction<
  TInput extends z.ZodType | NoInput,
  TOutput extends z.ZodType,
>(
  handler: (ctx: FunctionContext & {
    input: TInput extends z.ZodType ? z.infer<TInput> : any;
  }) => MaybePromise<z.infer<TOutput>>
): FunctionDef;
```

The first generic types `ctx.input` from the `input` export; the second types the return value from the `output` export. `NoInput` fills the first slot when a function validates output only:

```typescript
// functions/api/v1/stats.get.ts
import { z } from 'zod';
import { defineFunction, NoInput } from '@trayai/helix-sdk';

export const output = z.object({
  totalUsers: z.number(),
  activeToday: z.number(),
});

export default defineFunction<NoInput, typeof output>(async (ctx) => {
  return { totalUsers: 1234, activeToday: 56 };
});
```

A typical function with input and output validation:

```typescript
// functions/api/v1/users.post.ts
import { z } from 'zod';
import { defineFunction } from '@trayai/helix-sdk';

export const input = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  role: z.enum(['admin', 'member', 'viewer']).default('member'),
});

export default defineFunction<typeof input>(async (ctx) => {
  const kv = ctx.kv();
  const user = { id: crypto.randomUUID(), ...ctx.input };
  await kv.setItem(`user:${user.id}`, user);
  ctx.status(201);
  return user;
});
```

Return an object and the runtime sends it as JSON with status 200. Use `ctx.status()` to change the status, `throw ctx.error(status, message)` for errors, or return a `Response` object for full control (custom headers, non-JSON bodies, streaming).

## defineSchedule

Runs on a cron schedule. Files live in `functions/_scheduled/` and are never routable. A schedule exists whenever its file exists; there is no `enabled` flag, so remove the file to delete the schedule on the next deploy. Pausing and resuming without a deploy happens platform-side, and paused state survives redeploys.

```typescript
// functions/_scheduled/daily-report.ts
import { defineSchedule } from '@trayai/helix-sdk';

export default defineSchedule({
  cron: '0 9 * * MON-FRI',
  timezone: 'Europe/London',
  handler: async (ctx) => {
    ctx.log.info('Report run', { scheduledAt: ctx.trigger.scheduledAt });
  },
});
```

| Option | Type | Required | Default | Description |
|---|---|---|---|---|
| `cron` | `string` | Yes | none | Five-field standard cron expression: minute, hour, day, month, weekday. Month and weekday names are accepted; `?` is treated as `*`. |
| `timezone` | `string` | No | UTC | Named IANA timezone, for example `'Europe/London'`. Offset forms like `'+05:00'` are rejected. |
| `handler` | `function` | Yes | none | Receives `ctx` with all platform services but no request data: `ctx.input` and `ctx.method` are not available. The return value is discarded. |

Schedules are validated at build time, fail-closed: an invalid file fails the whole build with every problem listed. A cron expression cannot constrain day-of-month and day-of-week together, six-field expressions are rejected, and a project can hold at most 100 schedule files.

`ctx.trigger` carries `scheduledAt` (when the cron said to fire), `firedAt` (when it actually started), and `isManual` (always `false` today). A failed run is retried once without being marked as a retry, and overlapping runs are allowed, so write handlers to be idempotent. See [the scheduled functions guide](/documentation/guides/scheduled-functions/).

## Named exports read by the runtime

| Export | When it runs | On failure |
|---|---|---|
| `input` | Before the handler; merged request data is validated | 400 |
| `output` | After the handler; the return value is validated | 500 |

If a file declares none of these, no validation occurs and the handler receives raw, unvalidated data.

### input

A Zod schema validating everything coming into the function. The runtime merges three sources into one object, validates it, and puts the result on `ctx.input`:

```
Path params   { userId: "abc-123" }
Query params  { page: "2" }
Request body  { name: "Alice" }
Merged        { userId: "abc-123", page: "2", name: "Alice" }
```

Merge priority on key conflicts: path params beat body, body beats query params. Query params arrive as strings, so use `z.coerce.number()` where numbers are expected. Validation failure returns 400 before the handler runs.

### output

A Zod schema validating the handler's return value after it completes. Failure returns 500, since an invalid response indicates a bug in the function, not the caller. The `output` schema is not applied to streaming responses: a `Response` with a `ReadableStream` body can't be validated before it has been produced, so validation only runs on plain objects and JSON.

## Routing rules

The file system is the router: file path becomes URL path, file suffix becomes HTTP method, and directory nesting creates path segments.

| Rule | Behavior |
|---|---|
| Method suffix (`.get.ts`, `.post.ts`, `.put.ts`, `.patch.ts`, `.delete.ts`) | The file handles that HTTP method only. `functions/api/v1/users.post.ts` handles `POST /api/v1/users`. |
| No method suffix (`.ts`) | Catch-all: handles every method on the path. Branch on `ctx.method`. Prefer method-specific files except for webhooks. |
| `[param]` in a file or folder name | Dynamic segment. `functions/api/v1/users/[userId].get.ts` handles `GET /api/v1/users/:userId`; the value arrives merged into `ctx.input`. |
| Underscore prefix (`_`) | Excluded from routing: `_shared/` (importable utilities), `_scheduled/` (cron functions). |
| Defined path, undefined method | Automatic `405 Method Not Allowed`. If only `users.get.ts` and `users.post.ts` exist, `DELETE /api/v1/users` returns 405. |
| `users.get.ts` and `users/index.get.ts` both exist | `index.get.ts` takes priority. |
| Route priority | Function routes match first, then static files from `app/public/` or built SPA assets, then the SPA fallback (`app/index.html`). Functions always win over SPA routes. |

---

Canonical: https://helix.tray.ai/documentation/reference/functions/
Any link on this page is available as markdown by appending .md to its URL.
Full corpus: https://helix.tray.ai/documentation/llms-full.txt