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