Skip to content

Pre-GA Design Partner and Early Access only. Request access

Helix Docs

Function definition reference

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

For developers Updated Aug 5, 2026
View as Markdown

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.

HelperTriggerLocation
defineFunctionIncoming HTTP requestAnywhere routable under functions/
defineScheduleCron schedulefunctions/_scheduled/
defineQueueConsumerQueue messagefunctions/_queues/<queue-name>.ts
defineAppTriggerWebhook from a third-party app via HelixRoutable, for example functions/webhooks/
defineMiddlewareRuns 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.

// @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:

// 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:

// 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.

// 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 });
  },
});
OptionTypeRequiredDefaultDescription
cronstringYesnoneFive-field cron expression: minute, hour, day, month, weekday.
timezonestringNoUTCIANA timezone, for example 'Europe/London'.
concurrentbooleanNofalseWhen false, a trigger is skipped (with a logged warning) if the previous execution is still running. Set true when overlapping runs are safe.
handlerfunctionYesnoneReceives 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.

// 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,
    });
  },
});
OptionTypeRequiredDefaultDescription
retriesnumberNo3Maximum retry attempts after a failed execution.
backoff'fixed' | 'exponential'No'exponential'Retry delay strategy.
backoffDelaynumber (ms)No1000Base delay between retries. Doubles each retry with exponential.
timeoutnumber (ms)No30000Maximum execution time per message.
concurrencynumberNo1Maximum parallel invocations. With fifo: true this is concurrency across groups; each group stays serial.
rateLimit{ maxPerSecond?: number }NooffThrottles how many new messages start per second, independent of concurrency.
fifobooleanNofalseProcesses messages with the same groupId in publish order, one in flight per group.
handler(ctx) => Promise<void>YesnoneReceives 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.

// 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 });
  },
});
OptionTypeRequiredDescription
connectorstringYesConnector name from the Helix connector catalog, for example 'shopify'.
eventstringYesEvent to subscribe to, for example 'order.created'.
authenticationstringYesAuth alias from helix.config.ts used to register the webhook.
handlerfunctionYesReceives 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.

// 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

ExportWhen it runsOn failure
inputBefore the handler; merged request data is validated400
outputAfter the handler; the return value is validated500
accessBefore the handler; role check via Helix Identity403
toolRead at build and discovery time; MCP tool metadatan/a
rateLimitEnforced by the MCP service per tool callCall 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" }.

// functions/api/v1/settings.put.ts
export const access = {
  allowRoles: ['admin'],
};
FieldTypeBehavior
allowRolesstring[]Allowed only if the user holds at least one listed role.
denyRolesstring[]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. Tool files don’t need method suffixes, since tools are invoked over MCP, not HTTP.

// 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,
};
FieldTypeRequiredDescription
namestringYesTool name shown to agents; snake_case by convention.
descriptionstringYesWhat the tool does and when to use it. Agents choose tools by this text, so be specific.
inputSchemaZod schemaYesConverted to JSON Schema for MCP discovery. Usually the same schema as the input export.
examples{ input, output }[]NoSample calls that show agents expected inputs and outputs.
streamingbooleanNoWhen 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:

// functions/tools/expensive-operation.ts
export const rateLimit = {
  requestsPerMinute: 5,
  requestsPerHour: 50,
};
FieldTypeDescription
requestsPerMinutenumberMaximum invocations of this tool per minute.
requestsPerHournumberMaximum 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.

RuleBehavior
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 nameDynamic 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 methodAutomatic 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 existindex.get.ts takes priority.
Route priorityFunction 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.

Loading search…

Jump to a section

tab to move · esc to close