Capabilities

capability() declares one named application action — its input contract, authorization and approval rules, and the function Kaji executes.

A capability declares one application action: its validated input contract, authorization and approval rules, and the ordinary application function Kaji will eventually execute. A capability never executes itself — only kaji.execute() calls its execute function.

import { capability } from "@irogane/kaji";
import { z } from "zod";

const refund = capability({
  name: "payments.refund",
  input: z.object({ paymentId: z.string(), amount: z.number() }),

  authorize: async ({ principalId, input }) => {
    return canRefund(principalId, input.paymentId);
  },

  approval: ({ input }) => input.amount >= 500,

  execute: async (input, context) => {
    return refundPayment({
      ...input,
      idempotencyKey: context.idempotencyKey,
      signal: context.signal,
    });
  },
});

Fields

  • name is a stable, non-empty, application-defined identifier. It’s the only field the public Capability type exposes.
  • input needs only a parse(input: unknown) method that returns validated input or throws. Kaji does not select or export a schema library — Zod, Valibot, or a hand-written parser all satisfy this shape.
  • authorize receives validated input and returns whether the principal may execute the capability. Returning false denies execution. Throwing also denies it, before any side effect runs.
  • approval is optional. Returning true requires an approval decision before execute runs; returning false (or omitting approval) does not. The capability decides when approval is required — the executor enforces it. See Approval.
  • execute is ordinary application code. It receives validated input and the per-execution ExecutionContext (principalId, idempotencyKey, signal), and must use the supplied AbortSignal cooperatively when the underlying operation supports it.

Non-Goals

capability() only declares the action — it validates its own declaration (a non-empty name, an authorize function) at construction time, but never calls authorize, approval, or execute itself. Business logic stays in your application: Kaji must not embed domain-specific behavior such as a refund policy, and a capability’s execute function must remain ordinary application code that Kaji only calls through its executor.

Further Reading

  • Execution for how kaji.execute() calls these hooks in order
  • Approval for the approval boundary