Kaja

Documentation

Set up Kaja

Install Kaja, connect an API, and run your first call.

Desktop
Install Kaja and add your APIs from the sidebar.
Docker
Put your APIs, variables and scripts in kaja.json, and run the container.

Installation

Install Kaja from the Mac App Store.

Apps

An app is one API Kaja can call. Its services and methods appear in the sidebar under the app's name.

Add an app with + beside Apps in the sidebar. Pick the type, then give Kaja the address and the authentication it needs.

gRPC
Connect to a server using reflection or .proto files.
OpenAPI
Add a spec from a URL, a file, or pasted text.
Twirp
Connect to a server using its address and .proto files.

Kaja tests the connection before saving. Headers entered on the form are sent with every call.

If the app won't connect

“The server answered, but doesn't serve the reflection API.”

The gRPC server is reachable, but it does not register the reflection service. Click Use proto files in the banner and point Kaja at a folder of .proto files instead.

“Couldn't parse the document”, or “That URL returned a web page, not an OpenAPI document”

The URL is serving HTML, such as a documentation site or a sign-in redirect, or the file is not valid JSON or YAML. Open the URL in a browser and check that it returns the spec itself. If it needs a credential, switch the source to File or Pasteand give Kaja the document directly.

Run your first call

Once an app is connected, four steps get you a response.

1. Open an app in the sidebar and click one of its methods.

2. Kaja writes a typed call into a new draft and opens it in the editor. Every field of the request is written out, so you can see what the method takes before you change anything.

The call Kaja writes when you click ListMovies in the tree.

3. Press ⌘⏎, or click Run at the top right.

4. The run opens below the editor on Calls. Click the call to read the request Kaja sent and the response it got back.

Writing scripts

Scripts let you call APIs with TypeScript. Click a method to create a typed call, or start with an empty script. Press ⌘⏎ to run it. Save the script when you want to use it again.

scripts/a-night-out.ts
import { kaja } from "kaja";
import { Theatre } from "theatre";
import { Seating } from "seating";

const { shows } = await Theatre.ListShows({ city: "Chicago" });
kaja.table(["show", "starts"], shows.map((show) => [show.id, show.startsAt]));

await kaja.approve(
  Seating.BookSeats({ showId: shows[0].id, seatIds: ["F7", "F8"] }),
);

A script is TypeScript with top-level await and one import for each app. Results appear below the editor:

Calls
Every request and response.
Canvas
Tables, text, questions and anything else the script drew.
Stats
Latency and timing for the run.

Scripts also get fetch and console, both bound to the current run. A fetch request appears in Calls like any other call.

Every generated method takes headers as its second argument. Call .withHeaders() when you also need the headers the API answered with.

⌘P finds a method, file or draft by name.

Kaja helpers

A script can import kaja to draw on the run's canvas, ask you a question, or hold a call for approval. The table lists every helper. The entries under it have the detail.

Helper
Use it to
Show tabular results, including paged data.
Add text or code to the canvas.
Ask the user for input while a script runs.
Require approval before sending a call.
Run another script from a table cell.
Keep calls within an API's rate limit.
Run a timed performance test.
Read input passed to the script.
Read workspace variables.
Generate a UUID.
Build protobuf values from JavaScript values.
kaja.tableDraw a table on the canvas. You can add rows and update them while the script runs. A cell can also be a promise or a function, and Kaja fills it in when the work finishes.For a large result set, pass a row source instead of an array: an async generator that yields a page at a time. Kaja pages through it and searches it.
scripts/movies.ts
import { kaja } from "kaja";
import { Theatre } from "theatre";

const catalog = kaja.table(["id", "title"], async function* () {
  for (let cursor = ""; ; ) {
    const page = await Theatre.ListMovies({ cursor });
    catalog.total(page.total);
    yield* page.movies.map((movie) => [movie.id, movie.title]);
    if (!(cursor = page.nextCursor)) return;
  }
});
What that script draws. The rows are one page of a thousand; the box searches them.
kaja.text, kaja.codeAdd a line of text or a block of code to the canvas.
kaja.askStr, askInt, askSelectPause the script and ask the user for text, a whole number, or a choice from a list. The question appears on the canvas, and the script continues after the user answers.askSelect returns the value attached to the option that was picked, including an object if you provided one.
scripts/a-night-out.ts
import { kaja } from "kaja";
import { Theatre } from "theatre";

const { theaters } = await Theatre.ListTheaters({ city: "" });
const cities = [...new Set(theaters.map((theater) => theater.city))];

const city = await kaja.askSelect(
  "Where are you tonight?",
  cities.sort().map((name) => ({ label: name, value: name })),
);
const mood = await kaja.askStr("And what do you feel like?");
const party = await kaja.askInt("How many of you?");
That script, two answers in. An answered question keeps its answer and the run waits on the next one.
kaja.approveRequire approval before sending a call. Use this for operations that create, update, or delete data.
The request the call would send, drawn where the run stopped. Nothing leaves until Approve is pressed.
kaja.runPut a cell in a table that runs another script when you click it.
kaja.rateLimitKeep calls within the rate limit an API publishes. Nothing is paced until you call this. After that Kaja spreads the calls out, and waits when the remaining budget is spent.Kaja reads the budget from the response headers: RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, the X-RateLimit- and X-Rate-Limit- spellings of the same three, and Retry-After. For an API that publishes none of them, set a ceiling yourself with the perSecond option.
scripts/rate-limit.ts
import { kaja } from "kaja";
import { Theatre } from "theatre";
import { Seating } from "seating";

const { shows } = await Theatre.ListShows({ limit: 1 });
kaja.rateLimit(Seating);

const reads = kaja.table(["read", "remaining"]);
for (let read = 1; read <= 150; read++) {
  const { headers } = await Seating.GetSeatMap({
    showId: shows[0].id,
  }).withHeaders();
  reads.row(read, headers["ratelimit-remaining"] ?? "—");
}
What the limiter draws while that loop runs: the budget, and what obeying it has cost so far.
kaja.perfTestRun a function over and over on a schedule: a duration or a number of iterations, plus concurrency, warmup and ramp-up. Kaja opens the run on its Stats page.
scripts/how-fast.ts
import { kaja } from "kaja";
import { Theatre } from "theatre";

const report = await kaja.perfTest(
  () => Theatre.ListMovies({ limit: 25 }),
  { duration: "12s", concurrency: 6, warmup: "2s", rampUp: "4s" },
);

kaja.text(`p99 ${Math.round(report.latency.p99 ?? 0)} ms`);
Where that run opens. The bands behind the latency are the schedule it was given.
kaja.inputRead the values a deeplink or a kaja.run cell passed to this run, by name: kaja.input.city.Every value arrives as a string. Run repeats the values the script last ran with, so a script keeps what it was last given. Run with parameters opens those values for editing first.
kaja.variablesRead the workspace's variables. A variable stored in the Keychain, or read from an environment variable, works the same way as one typed into the workspace.
kaja.uuidV4Generate a random UUID. Inside a script, crypto.randomUUID() is this same function.
kaja.value, struct, listValueConvert a plain JavaScript value to a google.protobuf.Value, Struct or ListValue. Kaja builds the oneof fields for you.

The editor uses these declarations for autocomplete and inline documentation, and an agent reads the same ones when it writes a script. The whole of it is the kaja module declaration.

Files and drafts

An unsaved script appears under Drafts in the sidebar. Choose Save as file, or press ⌘S, to write it to disk under Files.

Reveal in Finder opens the folder the file is in. Edit a file in another editor and Kaja picks up the change the next time you run it.

Variables

Variables are named values shared by scripts and app configuration. Read one in a script with kaja.variables.NAME. Use ${NAME} in app configuration, either by itself or inside a longer value.

Add variables in the Variables section, { } at the top of the sidebar. Each row has a source:

Value
Stored directly in the workspace.
Keychain
Stored in the macOS Keychain. Use this for tokens and other secrets.
Environment
Read from an environment variable.

Agents

Kaja provides an MCP server for your connected APIs. An agent can inspect the available methods, write TypeScript scripts, and run them through Kaja.

Open the plug menu at the top of the sidebar and turn on the MCP server. Copy the command or configuration shown for your agent.

The server listens on 127.0.0.1:41521.

list_services
One TypeScript signature per method, marked read or write.
describe_method
The types a method uses, and an example call.
run_script
Runs a script. Every request and response comes back in the result.

Scripts an agent creates appear as drafts under the agent's name. Its requests appear in Calls. A call wrapped in kaja.approve waits until you approve it.