# Function definition reference

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

A Helix function is a TypeScript file that default-exports one of five `define*` helpers from `@trayai/helix-sdk`. The file's location and suffix determine its trigger and route, and named exports (`input`, `output`, `access`, `tool`, `rateLimit`) are read by the runtime for validation, access control, and MCP metadata. This page gives exact signatures, every option with its default, and a summary of the routing rules.

:::roadmap{title="Only defineFunction ships"}
HTTP is the only trigger Helix runs today, so `defineFunction` is the only helper you can use. `defineSchedule`, `defineAppTrigger`, `defineQueueConsumer`, and the `access` export belong to the platform design. App access is set per project on the dashboard's Access Control tab instead; see [identity and roles](/documentation/guides/identity-and-roles/) and [limits and roadmap](/documentation/reference/limits/).
:::

| Helper | Trigger | Location |
|---|---|---|
| `defineFunction` | Incoming HTTP request | Anywhere routable under `functions/` |
| `defineSchedule` | Cron schedule | `functions/_scheduled/` |
| `defineQueueConsumer` | Queue message | `functions/_queues/<queue-name>.ts` |
| `defineAppTrigger` | Webhook from a third-party app via Helix | Routable, for example `functions/webhooks/` |
| `defineMiddleware` | Runs before route handlers | `_middleware.ts` at any directory level |

## 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';
import { users } from '@project/db/schema';

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 [user] = await ctx.db().insert(users).values(ctx.input).returning();
  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 is active whenever its file exists; there is no `enabled` flag, so remove or rename the file to disable it.

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

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

| Option | Type | Required | Default | Description |
|---|---|---|---|---|
| `cron` | `string` | Yes | none | Five-field cron expression: minute, hour, day, month, weekday. |
| `timezone` | `string` | No | UTC | IANA timezone, for example `'Europe/London'`. |
| `concurrent` | `boolean` | No | `false` | When `false`, a trigger is skipped (with a logged warning) if the previous execution is still running. Set `true` when overlapping runs are safe. |
| `handler` | `function` | Yes | none | Receives `ctx` with all platform services but no request data: `ctx.input` and `ctx.method` are not available. |

`ctx.trigger` carries `scheduledAt` (when the cron said to fire), `firedAt` (when it actually ran), `isRetry`, and `isManual` (set by `helix trigger _scheduled/<name>`).

## defineQueueConsumer

Consumes messages from a queue declared in `helix.config.ts`. The filename selects the queue: `functions/_queues/order-processing.ts` consumes from the `order-processing` queue. Returning from the handler acknowledges the message; throwing returns it to the queue for retry, and messages that exhaust all retries move to the dead letter queue.

```typescript
// functions/_queues/order-processing.ts
import { z } from 'zod';
import { defineQueueConsumer } from '@trayai/helix-sdk';

export const input = z.object({
  orderId: z.string(),
  action: z.enum(['fulfill', 'cancel', 'refund']),
});

export default defineQueueConsumer<typeof input>({
  retries: 3,
  backoff: 'exponential',
  backoffDelay: 1000,
  timeout: 30_000,
  concurrency: 5,
  handler: async (ctx) => {
    ctx.log.info('Processing', {
      orderId: ctx.input.orderId,
      attempt: ctx.message.attempt,
    });
  },
});
```

| Option | Type | Required | Default | Description |
|---|---|---|---|---|
| `retries` | `number` | No | `3` | Maximum retry attempts after a failed execution. |
| `backoff` | `'fixed' \| 'exponential'` | No | `'exponential'` | Retry delay strategy. |
| `backoffDelay` | `number` (ms) | No | `1000` | Base delay between retries. Doubles each retry with `exponential`. |
| `timeout` | `number` (ms) | No | `30000` | Maximum execution time per message. |
| `concurrency` | `number` | No | `1` | Maximum parallel invocations. With `fifo: true` this is concurrency across groups; each group stays serial. |
| `rateLimit` | `{ maxPerSecond?: number }` | No | off | Throttles how many new messages start per second, independent of `concurrency`. |
| `fifo` | `boolean` | No | `false` | Processes messages with the same `groupId` in publish order, one in flight per group. |
| `handler` | `(ctx) => Promise<void>` | Yes | none | Receives the standard context plus `ctx.input` (the validated payload) and `ctx.message`. |

The optional `input` export validates the message payload with Zod before the handler runs. `ctx.message` carries `id`, `queue`, `attempt` (1-based), `maxAttempts`, `publishedAt`, and `metadata` from the publisher. The platform enforces concurrency, rate limits, and FIFO ordering; handler code never implements them.

## defineAppTrigger

Reacts to events in third-party apps. Helix registers and manages the webhook in the external system through its trigger API; your function runs when the event fires.

```typescript
// functions/webhooks/shopify.ts
import { defineAppTrigger } from '@trayai/helix-sdk';

export default defineAppTrigger({
  connector: 'shopify',
  event: 'order.created',
  authentication: 'shopify_prod',
  handler: async (ctx) => {
    const order = ctx.trigger.data;
    ctx.log.info('New order', { id: order.id });
  },
});
```

| Option | Type | Required | Description |
|---|---|---|---|
| `connector` | `string` | Yes | Connector name from the Helix connector catalog, for example `'shopify'`. |
| `event` | `string` | Yes | Event to subscribe to, for example `'order.created'`. |
| `authentication` | `string` | Yes | Auth alias from `helix.config.ts` used to register the webhook. |
| `handler` | `function` | Yes | Receives the event payload on `ctx.trigger.data`. |

You don't define an input schema for app triggers: the payload shape comes from the Helix connector catalog. Run `helix generate-types` to pull the catalog schemas and generate TypeScript types for `ctx.trigger.data`.

## defineMiddleware

Files named `_middleware.ts` apply to all routes at and below their directory. They follow the standard `(ctx, next)` pattern: run code, call `await next()` to pass control down, then run code after the response.

```typescript
// functions/api/v1/_middleware.ts
import { defineMiddleware } from '@trayai/helix-sdk';

export default defineMiddleware(async (ctx, next) => {
  const apiKey = ctx.headers.get('x-api-key');
  if (!apiKey) throw ctx.error(401, 'API key required');

  ctx.state.apiKey = apiKey;  // visible to downstream handlers
  await next();
});
```

Middleware nests by directory: `functions/_middleware.ts` runs first, then `functions/api/v1/_middleware.ts`, then the handler. Throwing before `next()` short-circuits the request.

## 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 |
| `access` | Before the handler; role check via Helix Identity | 403 |
| `tool` | Read at build and discovery time; MCP tool metadata | n/a |
| `rateLimit` | Enforced by the MCP service per tool call | Call rejected |

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.

### access

Restricts an HTTP function by role. Requires Helix Identity; the runtime enforces the rule before the handler runs and rejects unauthorized requests with 403 and a JSON body of the form `{ "error": "forbidden", "message": "This endpoint requires one of the following roles: admin" }`.

```typescript
// functions/api/v1/settings.put.ts
export const access = {
  allowRoles: ['admin'],
};
```

| Field | Type | Behavior |
|---|---|---|
| `allowRoles` | `string[]` | Allowed only if the user holds at least one listed role. |
| `denyRoles` | `string[]` | Rejected if the user holds any listed role. |

Evaluation rules:

- Deny wins. `denyRoles` is checked first; a user holding a denied role is rejected even if they also hold an allowed role.
- No `access` export means no role restriction: any authenticated user can call the function.
- `allowRoles: []` is a build-time error (it would reject everyone); omit the field instead.
- Role IDs are validated against the `roles` map in `helix.config.ts` at build time. An undefined role fails `helix deploy` and surfaces as an error in `helix dev`.
- `access` applies to HTTP functions only. Schedules, app triggers, and queue consumers run without a user, so an `access` export in those files is a build-time error. So is `access` on a route excluded from authentication, where `ctx.identity` is `null`.

`access` rules are enforced in `helix dev` exactly as in production: same 403, same body.

### tool

MCP metadata for a function exposed as a tool. The function must also be listed in the `mcp.tools` array in `helix.config.ts`; see the [helix.config.ts reference](/documentation/reference/configuration/). Tool files don't need method suffixes, since tools are invoked over MCP, not HTTP.

```typescript
// functions/tools/lookup-customer.ts
import { z } from 'zod';

export const input = z.object({
  email: z.string().email(),
});

export const tool = {
  name: 'lookup_customer',
  description: 'Look up customer information by email address',
  inputSchema: input,
};
```

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | `string` | Yes | Tool name shown to agents; snake_case by convention. |
| `description` | `string` | Yes | What the tool does and when to use it. Agents choose tools by this text, so be specific. |
| `inputSchema` | Zod schema | Yes | Converted to JSON Schema for MCP discovery. Usually the same schema as the `input` export. |
| `examples` | `{ input, output }[]` | No | Sample calls that show agents expected inputs and outputs. |
| `streaming` | `boolean` | No | When `true`, the handler can push progress updates with `ctx.stream()` before returning its final result. |

### rateLimit

Per-function override of the MCP server's rate limit for this tool:

```typescript
// functions/tools/expensive-operation.ts
export const rateLimit = {
  requestsPerMinute: 5,
  requestsPerHour: 50,
};
```

| Field | Type | Description |
|---|---|---|
| `requestsPerMinute` | `number` | Maximum invocations of this tool per minute. |
| `requestsPerHour` | `number` | Maximum invocations of this tool per hour. |

## 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), `_queues/` (queue consumers), `_middleware.ts` (middleware). |
| 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