Private beta. We're working directly with early teams. Talk to a founder
TailorKit

Actions

Define server-side functions that TailorKit apps can call for trusted work.

Actions are server-side functions exposed by your host app. Use them for work that must stay under your control: API calls, database writes, permission checks, payments, invites, role changes, and other trusted side effects.

Callbacks are for UI events. Actions are for trusted work.

Create An Action Builder

Use createActions() to create an action builder. If your actions need access to host state, add a context type with .context<...>().

./src/lib/tailorkit.ts
import { createActions } from "tailorkit";

const action = createActions().context<{
  user: { id: string; name: string };
}>();

The context type controls the context value passed to every action created from this builder.

Define An Action

An action is built from input, output, and a server-side handler.

./src/lib/tailorkit.ts
import { createActions, createTailorKit } from "tailorkit";
import { z } from "zod";

const action = createActions().context<{
  user: { id: string; name: string };
}>();

export const tailorKit = createTailorKit({
  components: {},

  actions: {
    echo: action
      .input(z.string())
      .output(z.string())
      .handler(({ input, context }) => `${context.user.name} said '${input}'`),
  },
});

The handler runs on your server. It receives validated input and the context you pass from your TailorKit API handler.

Input And Output

Define input and output schemas explicitly. TailorKit uses them to generate the contract between TailorKit apps and your backend, and to validate data at the action boundary.

const createTodo = action
  .input(
    z.object({
      title: z.string().min(1),
    }),
  )
  .output(
    z.object({
      id: z.string(),
      title: z.string(),
    }),
  )
  .handler(async ({ input, context }) => {
    const todo = await db.todo.create({
      data: {
        title: input.title,
        userId: context.user.id,
      },
    });

    return {
      id: todo.id,
      title: todo.title,
    };
  });

Keep The Contract Explicit

The schemas are not just TypeScript hints. They describe what TailorKit apps are allowed to send and what your backend promises to return.

Pass Context From The Handler

Pass an authenticate function when you call tailorKit.handler. TailorKit calls it for routes that need your app's authenticated user, then passes the returned actionContext to your action handlers.

app/api/tailorkit/[...tailorkit]/route.ts
import { auth } from "#lib/auth";
import { tailorKit } from "#lib/tailorkit";

async function handleRequest(request: Request) {
  return tailorKit.handler(request, {
    authenticate: async () => {
      const session = await auth(request);

      if (!session) {
        return null;
      }

      return {
        actionContext: {
          user: {
            id: session.user.id,
            name: session.user.name,
          },
        },
        scopeId: session.user.id,
      };
    },
  });
}

export const GET = handleRequest;
export const POST = handleRequest;

actionContext is passed to your action handlers. scopeId separates installed apps by user, team, project, or whatever boundary makes sense for your product. Return null when the request is not authenticated; TailorKit will reject host authenticated routes while still allowing TailorKit-managed deploy-token routes to use their own authentication.

Organize Actions

Small projects can define actions directly in tailorkit.ts. Larger projects are easier to maintain when actions live in their own files.

src/
  lib/
    tailorkit.ts
    tailorkit/
      actions/
        index.ts
        messages.ts
        projects.ts

Create one shared action builder for your context type:

./src/lib/tailorkit/actions/index.ts
import { createActions } from "tailorkit";

export const action = createActions().context<{
  user: { id: string; name: string };
}>();

export { messageActions } from "./messages";
export { projectActions } from "./projects";

Then define actions by domain:

./src/lib/tailorkit/actions/messages.ts
import { z } from "zod";
import { action } from "./index";

export const messageActions = {
  send: action
    .input(
      z.object({
        body: z.string().min(1),
        channelId: z.string(),
      }),
    )
    .output(
      z.object({
        id: z.string(),
      }),
    )
    .handler(async ({ input, context }) => {
      const message = await db.message.create({
        data: {
          body: input.body,
          channelId: input.channelId,
          userId: context.user.id,
        },
      });

      return { id: message.id };
    }),
};

Nested Actions

Actions can be nested. This keeps larger action sets readable and gives apps a clear path to call.

./src/lib/tailorkit.ts
import { createTailorKit } from "tailorkit";
import { messageActions, projectActions } from "./tailorkit/actions";

export const tailorKit = createTailorKit({
  components: {},

  actions: {
    messages: messageActions,
    projects: projectActions,
  },
});

With this structure, apps can target actions by path, such as messages.send or projects.archive.

Use One Context Shape

All actions in one TailorKit instance must use the same context type. Create one shared action builder and reuse it across action files.

Actions And Callbacks

Use callbacks for component events such as onClick, onOpenChange, and onValueChange. Use actions when the app needs your backend to perform trusted work.

const Button = {
  callbacks: {
    onClick: {},
  },
  slots: ["default"],
};

A callback can tell the app that a button was clicked. An action can create a record, send an invite, charge a customer, or call an internal API.

Best Practices

  • Keep actions focused on one piece of trusted work.
  • Use narrow input schemas with required fields, enums, and bounded values.
  • Return only the data the TailorKit app needs.
  • Use context for identity, permissions, and tenant-aware queries.
  • Use scopeId to match the boundary where apps are installed.
  • Do not return secrets, tokens, internal IDs, or sensitive metadata unless the app truly needs them.