Getting Started

Define a capability and run it through the Kaji executor.

Kaji is TypeScript-only in v0 and is not yet published to a package registry. This tutorial defines one capability and executes it with the in-memory execution store, the same shape as examples/refund in the repository.

Install

From within the repository (or a project that depends on the @irogane/kaji workspace package):

bun install

Kaji does not require or declare a schema library as a dependency. input only needs a parse(input: unknown) method that returns validated input or throws — this example uses Zod, but any library with that shape works.

bun add zod

First run

  1. Define and execute a capability

    import { capability, createKaji, memoryStore } from "@irogane/kaji";
    import { z } from "zod";
    
    const echo = capability({
      name: "echo",
      input: z.object({ message: z.string() }),
      authorize: async () => true,
      execute: async (input) => ({ message: input.message }),
    });
    
    const kaji = createKaji({ store: memoryStore() });
    
    const result = await kaji.execute(echo, {
      input: { message: "Hello, Kaji." },
      principalId: "local-user",
      idempotencyKey: "echo-1",
    });
    
    console.log(result);

    Run it with bun run kaji.ts (or npx tsx kaji.ts). It prints a succeeded result carrying { message: "Hello, Kaji." } and execution evidence (executionId, capability, principalId, idempotencyKey, inputFingerprint).

  2. Add approval for sensitive input

    approval marks which requests need a decision before execute runs. Supply approve on the executor to decide them:

    const refund = capability({
      name: "payments.refund",
      input: z.object({ paymentId: z.string(), amount: z.number() }),
      authorize: async ({ principalId, input }) => canRefund(principalId, input.paymentId),
      approval: ({ input }) => input.amount >= 500,
      execute: async (input, context) => refundPayment(input, context),
    });
    
    const kaji = createKaji({
      store: memoryStore(),
      approve: async (request) => requestHumanApproval(request),
    });

    An absent approver, an invalid decision shape, a rejected decision, or an approver error settles the execution as rejected instead of running execute.

  3. Handle every outcome

    execute() resolves with an explicit status instead of throwing for expected outcomes: succeeded, denied, rejected, failed, cancelled, or unknown. unknown means a side effect may have committed but Kaji cannot prove it — do not blindly retry it.

    if (result.status === "succeeded") {
      console.log(result.result);
    } else if (result.status === "unknown") {
      // do not retry automatically; the side effect may have already run
    }

Further Reading

  • Capabilities for the full capability() contract
  • Execution for the exact order Kaji runs validation, authorization, approval, and execution
  • Idempotency for how retries and duplicates resolve
  • docs/api.md in the repository for the complete frozen contract, including the export boundary and known-failure semantics
  • examples/refund in the repository for a runnable end-to-end proof