Skip to content

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

Helix Docs

Request and response

Reference for request and response on ctx, covering validated input, headers, raw body access, status codes, errors, middleware state, and triggers.

For developers Updated Aug 5, 2026
View as Markdown

HTTP-triggered functions read their request through ctx.input (validated, merged input), ctx.method, ctx.headers, ctx.path, and ctx.request (raw body access). They respond by returning a value, setting ctx.status(), or throwing ctx.error(). Non-HTTP function types receive their payload on ctx.trigger instead.

ctx.input

ctx.input is one object merging path params, query params, and the request body. When the file has a named export input (a Zod schema), the runtime validates the merged object before the handler runs and returns 400 on failure; the generic defineFunction<typeof input> types ctx.input from the same schema. With no schema, ctx.input holds the raw merged object, unvalidated.

Merge priority on key conflicts: path params > body > query params. Path params always win because the URL structure defines them.

SourceExample
Path params{ userId: "abc-123" }
Query params{ page: "2", limit: "10" }
Request body{ name: "Alice" }
Merged, then validated{ userId: "abc-123", page: 2, limit: 10, name: "Alice" }

Query params arrive as strings; use z.coerce.number() for numeric fields. A named output export validates the return value after the handler completes (500 on failure); it is skipped for streaming responses.

ctx.method, ctx.headers, ctx.path

FieldTypeDescription
methodstringThe HTTP method. Branch on it in catch-all files (no method suffix).
headersHeadersStandard web Headers; read with ctx.headers.get('x-api-key').
pathstringThe request path.

ctx.request

Raw request access for advanced handling:

request: {
  isMultipart: boolean;                    // Content-Type is multipart/form-data
  isRaw: boolean;                          // body is not JSON or form data
  files: () => Promise<UploadedFile[]>;    // parsed multipart file parts
  buffer: () => Promise<ArrayBuffer>;      // whole body as bytes
  text: () => Promise<string>;             // whole body as text
  signal: AbortSignal;                     // fires when the client disconnects
}

Each uploaded file exposes its form fieldname, buffer, mimetype, size, and extension, so a handler can read an upload in memory and act on it. Helix ships no file storage to persist it to: ctx.files is platform design. signal is the cleanup hook for long-lived streams; see the streaming guide.

ctx.status() and ctx.error()

status(code: number): void
error(status: number, message: string): HttpError

ctx.status(code) sets the response status for the returned value (for example 201 after a create, or 204 with no return value). ctx.error(status, message) builds an HttpError for you to throw; the runtime turns it into the corresponding HTTP response.

ctx.state

ctx.state is a Record<string, any> for middleware-to-handler handoff. Middleware in functions/_middleware.ts (or a nested _middleware.ts) writes to it before calling next(); the handler reads it. See the functions and routing guide for middleware ordering.

// functions/api/v1/_middleware.ts sets ctx.state.apiKey; the handler reads it:
const apiKey = ctx.state.apiKey;

ctx.trigger by function type

defineFunction is the only function type in the shipped product, because HTTP is the only trigger. The other three rows describe platform design.

Function typectx.trigger contents
defineFunctionRequest data lives on ctx.input, ctx.method, ctx.headers, and ctx.request; trigger.data and trigger.scheduledAt are unavailable.
defineSchedule{ type: 'schedule', scheduledAt, firedAt, isRetry, isManual }. scheduledAt is when the cron said to fire, firedAt when it actually ran, isRetry marks a retried execution, isManual marks a helix trigger run.
defineAppTrigger{ type: 'app', data } where data is the webhook payload, typed from the connector catalog via helix generate-types.
defineQueueConsumerNo trigger data; the payload is ctx.input and delivery metadata is ctx.message.

Returning objects vs Response

Return a plain object and the runtime wraps it as JSON with status 200 (or whatever ctx.status() set). Return a Response for full control over status, headers, and body, including streaming bodies (ReadableStream), which work locally and in production via Lambda response streaming.

// functions/api/v1/users/[userId].put.ts
import { z } from 'zod';
import { defineFunction } from '@trayai/helix-sdk';

export const input = z.object({
  userId: z.string().uuid(),              // from path param [userId]
  name: z.string().min(1).optional(),     // from request body
  email: z.string().email().optional(),   // from request body
});

export default defineFunction<typeof input>(async (ctx) => {
  const { userId, ...updates } = ctx.input;
  const kv = ctx.kv();

  const user = await kv.getItem<Record<string, unknown>>(`user:${userId}`);
  if (!user) throw ctx.error(404, 'User not found');

  const updated = { ...user, ...updates };
  await kv.setItem(`user:${userId}`, updated);

  return updated; // 200 OK, JSON
});

And with a Response for a non-JSON body:

return new Response(csv, {
  status: 200,
  headers: {
    'Content-Type': 'text/csv',
    'Content-Disposition': 'attachment; filename="report.csv"',
  },
});

SSE patterns, transform streams, and platform limits for streamed responses are covered in the streaming guide.

Loading search…

Jump to a section

tab to move · esc to close