Skip to content
RELKIT
Esc
↑↓navigate↵open⌘Jpreview
On this page

Functions

Define reusable behavior once, invoke it through the common engine, and test it without a transport.

A function contains a piece of application behavior, such as calculating a price or creating an order. It declares runtime input and output schemas, expected errors, provider-backed dependencies, optional publication access, and one handler. Routes, jobs, event consumers, tools, agents, and direct calls all enter the same invocation engine.

Functions remain request- or event-scoped operations. When work must outlive the initiating request, define a task and expose it through a task-first job; do not turn a function target into a durable worker by adding queue fields.

Scaffold a callable function

With an existing orders service, run from the application root:

bun run relkit add function send-receipt --service orders

This creates src/services/orders/functions/send-receipt.function.ts and exposes sendReceipt through src/services/orders/service.ts. The placeholder accepts and returns a string value; replace its schemas and handler with your domain behavior. Its stable ID is orders.send-receipt.

Pass --internal to omit public service exposure. Use --create-service orders instead of --service orders only if the domain does not exist. No test files are generated.

For every scaffolding flag, see the function command reference.

Read a complete function

/** Validates order input, invokes the service-local pricing function, and returns the calculated order. */
import { defineFunction } from "@relkit/app/functions";
import { z } from "@relkit/app/schema";
import priceOrder from "@app/services/orders/functions/price-order.function.js";

const orderInput = z.object({
  orderId: z.string().min(1),
  sku: z.string().min(1),
  quantity: z.number().int().positive(),
});

const orderOutput = z.object({
  orderId: z.string(),
  sku: z.string(),
  totalCents: z.number().int().nonnegative(),
});

const createOrder = defineFunction({
  input: orderInput,
  output: orderOutput,

  handler: async (input) => {
    const { totalCents } = await priceOrder.invoke({ quantity: input.quantity });
    const order = {
      orderId: input.orderId,
      sku: input.sku,
      totalCents,
    };

    return order;
  },
});

export default createOrder;

The handler receives validated input. Its return value is validated before the invocation succeeds. The example calls priceOrder.invoke(...), so the nested operation keeps validation, deadlines, cancellation, tracing, and causation. Calling another descriptor’s handler directly bypasses that contract.

Use this function from the frontend

Expose createOrder through its Orders service and a route. The API example maps that public member to POST /orders:

/** Maps the HTTP endpoint to its owning service's public member. */
import { defineServiceRoutes } from "@relkit/app/routes";
import orders from "@app/services/orders/service.js";

export const { POST } = defineServiceRoutes(orders, {
  POST: { member: "createOrder", successStatus: 201 },
});

After frontend setup and pulling this API example’s contract, render this component under RelkitClientProvider:

"use client";

/** Calls the API example's Orders function through its public route, retaining the generated input and price result. */
import { useRelkitClient, useRouteMutation } from "@relkit/client/react";

/** Creates one order in the API example without importing the backend service.
 * @returns A guarded submission action and its typed price or failure. */
export function CreateOrder() {
  const { status } = useRelkitClient();
  const create = useRouteMutation("POST /orders");

  return (
    <section>
      <button
        disabled={status !== "ready" || !create.isIdle}
        onClick={() => create.mutate({ orderId: crypto.randomUUID(), sku: "book", quantity: 2 })}
      >
        Create an order
      </button>
      {create.isPending && <p role="status">Creating…</p>}
      {create.isSuccess && <p>Total: {create.data.totalCents} cents</p>}
      {create.isError && (
        <p role="alert">{create.error.message}. Check the outcome before retrying.</p>
      )}
    </section>
  );
}

This calls the same createOrder function through POST /orders: orderId, sku, and quantity come from its input schema, and totalCents comes from its output schema. The browser calls RPC; it never imports the descriptor or calls .invoke(). Service membership needs a client-exposed route to become a browser operation.

Reference: useRouteMutation. Continue with mutation state and recovery.

Define the boundary

Start with input and output

Put a schema beside its only consumer, or in the owning domain when several descriptors share it. Keep transport fields such as cookies and raw headers in the route unless they are genuine domain input.

Declare failures and capabilities

List only errors callers are expected to handle. Declare only the jobs, buckets, caches, and agents the handler uses. Event publication is separate: list exact event IDs in publishes.

Implement transport-independent behavior

Use the supplied context, honor its abort signal, and return the declared output. Let unexpected defects cross RELKIT’s safe boundary rather than converting them into unstable public errors.

Hooks and transport policy

onBefore transforms validated input before the handler. onAfter transforms successful output before final output validation. Use hooks only for function policy that must apply to every invocation source; use HTTP middleware for request-native policy.

Test the operation directly

/** Checks orders.function through the supported public RELKIT APIs. */
import { expect, test } from "bun:test";
import { invokeFunction } from "@relkit/testing";
import orders from "@app/services/orders/service.js";

test("createOrder invokes pricing", async () => {
  const result = await invokeFunction(orders.createOrder, {
    orderId: "order-1",
    sku: "book",
    quantity: 3,
  });

  expect(result).toEqual({ orderId: "order-1", sku: "book", totalCents: 300 });
});
bun test tests/unit/orders.function.test.ts

invokeFunction runs the descriptor through an isolated engine. A function that needs provider clients receives explicit test fakes; tests do not infer production replacements from RELKIT_ENV.

Reference and source