Skip to main content

Getting started

This page takes you from an empty folder to a running mekik server that streams generative UI, runs a journaled tool, and pauses for a human — then connects a real client to it. If you read one page, read this one.

Prerequisites​

  • Node ≥ 22 (mekik is developed on Node 26 and uses the built-in TypeScript runner — no build step to run a .ts file). For .NET, see the .NET section.
  • A compiled ilmek graph, or willingness to write a three-line one. If ilmek is new to you, its MODEL.md is the reference; mekik only needs a compile()d graph.

Step 1 — Install​

pnpm add @mekik/core @mekik/ws @ilmek/core

@mekik/core is the engine, mapper, helpers, stores, and auth port. @mekik/ws is the WebSocket transport. @ilmek/core is the graph runtime mekik serves — a peer you already depend on to author graphs.

Step 2 — Serve a graph​

The smallest useful server is a graph plus a WebSocket transport. Pick your language once — the choice follows you across every code sample on the site:

// server.ts
import { graph, channel, START, END } from "@ilmek/core";
import { mekik } from "@mekik/core";
import { serveWs } from "@mekik/ws";

const g = graph("echo")
.channel("input", channel.lastWrite<string>(""))
.channel("reply", channel.lastWrite<string>(""))
.node("reply", (s) => ({ reply: `You said: ${s.input}` }))
.edge(START, "reply").edge("reply", END)
.compile();

const app = mekik({
graph: g,
reply: (s) => s.reply as string, // pick the run's reply text from final state
});

serveWs(app, { port: 8800, path: "/ws" });
console.log("mekik on ws://localhost:8800/ws");
node server.ts

mekik({ graph }) / new MekikApp(new MekikOptions { Graph = graph }) builds a MekikApp with in-memory defaults for everything (checkpointer, history, conversations). The transport (serveWs / MapMekik) turns each socket into a connection the app drives. Omit the path to accept the upgrade on any path — handy when a client points at /chat while you were thinking /ws.

What a client sees​

On connect, the server sends a welcome frame announcing the protocol and the minted identity. Each text frame the client sends starts one run:

// client → server
{ "type": "text", "data": { "text": "hello" } }

// server → client
{ "type": "run", "data": { "status": "started" } }
{ "type": "text", "id": "msg-1", "seq": 1, "from": "bot",
"data": { "text": "You said: hello" }, "timestamp": 1750000000000 }
{ "type": "run", "data": { "status": "finished" } }

That's the whole loop. Everything below adds richer frames to it.

Step 3 — Add generative UI, a tool, and a pause​

The three authoring helpers are the reason to use mekik over a raw socket. Here's a node that uses all three:

import { mekik } from "@mekik/core";

// inside .node("refund", async (s, ctx) => { ... })
const id = s.input as string;
const order = await mekik.tool(ctx, "get_order", { id }, () =>
Orders.get(id), // ← runs exactly once, journaled
);

mekik.ui(ctx, "order-card", { id: order.id, total: order.total }); // ← stream a component

const ok = await mekik.approve<{ approved: boolean }>( // ← pause for a human
ctx,
{ title: `Refund ${order.total}?` },
{ ui: { component: "approval-form", props: { orderId: order.id } } },
);

return { reply: ok.approved ? "Refunded." : "Cancelled." };

Each helper maps to a frame on the wire:

HelperEmitsRead more
mekik.tool(ctx, name, params, fn)tool_call running → completed/error, and journals fnTools
mekik.ui(ctx, component, props)a genui ui chunkGenerative UI
mekik.text(ctx, content)a genui text chunk (streaming prose)Generative UI
mekik.approve(ctx, payload, opts)an interrupt frame; the run ends interruptedHuman-in-the-loop

The exactly-once guarantee is the point of mekik.tool: because a paused node re-runs from the top on resume, anything before the pause happens twice — unless it's journaled. mekik.tool journals it through ilmek's ctx.step, so the resume pass returns the recorded value instead of charging the card again. See Human-in-the-loop → exactly-once.

Step 4 — Configure the app​

mekik(options) takes a handful of options; all but graph have sensible defaults.

const app = mekik({
graph: g,

// Map an inbound text turn to the graph's input update. Default: { input: text }.
input: (frame) => ({ input: frame.data.text }),

// Pick the run's consolidated reply text from final channel state.
reply: (state) => state.reply as string,

// Per-turn server context, placed at ctx.meta.mekik for nodes to read.
context: (conv, turn) => ({ userId: conv.userId, locale: "en" }),

// A one-time bot message when a fresh conversation first connects.
greeting: (conv) => "Hi! Send an order number to start a refund.",
});

The greeting isn't limited to a paragraph. It also takes a described rich message, or a list mixing both — so a first impression can be a card with buttons, each item landing as its own persistent frame:

greeting: (conv) => [
`Hi ${conv.userId}! What can I do for you?`,
mekik.messages.buttons.spec({
buttons: [
{ label: "Track an order", value: "/track" },
{ label: "Start a return", value: "/return" },
],
}),
],

The full option list, with types, is in Concepts → The app. For durability, swap the in-memory checkpointer for a real one (see Persistence) — an interrupt lives in the checkpoint, so an in-memory one loses parked pauses on restart.

Step 5 — Connect a real client​

The client end is chativa's @chativa/connector-mekik, which already speaks mekik/1:

import { MekikConnector } from "@chativa/connector-mekik";
import { ConnectorRegistry, chatStore } from "@chativa/core";

ConnectorRegistry.register(
new MekikConnector({ url: "ws://localhost:8800/ws", resumeConversation: true }),
);
chatStore.getState().setConnector("mekik");

It renders text bubbles, mounts your GenUI components, shows tool_call traces, renders interrupt frames as approval chips (or a mounted form), and answers them with a resume — all with no per-app glue. See chativa's MekikConnector docs for the client-side options.

Prefer to drive it from a script? Any WebSocket client works — the wire is plain JSON frames. See Protocol → Frames for the shapes.

.NET in parallel​

The .NET port is the same shape — the .NET tab in Step 2 above serves a graph from ASP.NET Core with app.MapMekik("/ws", app). The authoring helpers are Shuttle.Ui, Shuttle.Tool, Shuttle.Approve (the class is Shuttle, not Mekik, to avoid a namespace clash — see Parity). The wire is byte-identical to TypeScript's, held there by the shared golden fixtures.

Try the examples​

The repo ships runnable examples that each exercise the whole stack. The refund example is the canonical showcase — a tool trace, a GenUI card, a form approval, a resume, and the exactly-once assertion, in one graph:

node ts/examples/refund.ts # in-memory self-test, asserts the exact wire trace
node ts/examples/refund.ts --serve # a real ws://localhost:8800 server (any path)

The --serve mode renders end-to-end against chativa's sandbox, whose component registry already has the order-card, approval-form, data-table, and weather-card these examples emit. See Examples for the full tour, including the LLM-driven agents.

Where to go next​