---
title: Basic Usage
description: Build a plug, register flows and webhook handlers, and use the runtime and CLI surfaces together.
section: Get Started
url: /docs/basic-usage
---

# Basic Usage

This walkthrough shows the package in the shape it is designed to be used today: a plug client, optional flows, optional webhook wiring, and the shared runtime behind `/api/khotan`.

## 1. Scaffold the pieces you need

```terminal
$ npx khotan add plug --yes
$ npx khotan add cache --yes
$ npx khotan add inflow --yes
$ npx khotan add wire --yes
$ npx khotan add catch --yes
$ npx khotan add pass --yes
$ npx khotan add mapping-browser --yes
$ npx khotan add mappings-page-1 --yes
$ npx khotan add hub --yes
$ npx khotan add runs --yes
```

You do not need every component in every project. This set is useful because it covers the main khotan model:

- a plug that talks to an external API
- a durable cache for cross-run snapshots, cursors, or dedupe markers
- a flow that pulls data on demand or on a schedule
- a wire that registers a webhook callback
- catch and pass handlers that receive verified webhook events
- UI to inspect and operate the system, including first-class mappings

## 2. Define a plug

A **plug** is a one-way HTTP client from your app to an external API. Use the generated `plug.ts` builder to create a real service client.

```ts
// src/khotan/plugs/pollinate.ts
import { plug, bearer } from "@/khotan/plugs/plug";

export const pollinatePlug = plug({
  name: "pollinate",
  baseUrl: "https://api.pollinate.tech",
  auth: bearer(process.env.POLLINATE_TOKEN!),
  vars: [
    { key: "orgId", label: "Organization ID", type: "text" },
  ] as const,
  endpoints: {
    listProducts: {
      method: "GET",
      path: "/products",
    },
  },
});
```

What the plug gives you:

- auth helpers
- retry and timeout support
- pagination helpers
- optional encrypted vars
- optional typed endpoint metadata for the debugger and CLI

## 3. Define a cache

Use a **cache** when a flow or webhook workflow needs durable state between runs.

```ts
// src/khotan/caches/pollinate-products-snapshot.ts
import { cache } from "@/khotan/caches/cache";

export const pollinateProductsSnapshotCache = cache({
  name: "pollinate-products-snapshot",
  scope: {
    plug: "pollinate",
    resource: "products",
    flow: "pollinate-products",
  },
  ttl: "6h",
});
```

This gives your workflows a named store that you can access with `khotanCache(ctx, "pollinate-products-snapshot")`.

## 4. Define a flow

Flows are the durable data-movement primitives. Here is a simple inflow:

```ts
// src/khotan/flows/pollinate-products.ts
import { khotanCache } from "khotan-data/factory";
import { inflow, type InflowContext } from "@/khotan/flows/inflow";
import { pollinatePlug } from "@/khotan/plugs/pollinate";

async function pollinateProductsWorkflow(ctx: InflowContext) {
  "use workflow";

  async function syncProducts() {
    "use step";
    const snapshotCache = khotanCache(ctx, "pollinate-products-snapshot");
    const previous =
      (await snapshotCache.get<Array<{ id: string; sku?: string }>>("latest")) ?? [];
    const response = await pollinatePlug.get<{ data?: Array<{ id: string; sku?: string }> }>("/products", {
      vars: ctx.vars,
    });
    const records = Array.isArray(response.data) ? response.data : [];

    await snapshotCache.set("latest", records);

    return {
      extracted: records.length,
      transformed: records.length,
      created: records.length,
      metadata: {
        source: ctx.flow.name,
        previousCount: previous.length,
      },
    };
  }

  return syncProducts();
}

export const pollinateProductsInflow = inflow({
  name: "pollinate-products",
  resource: "products",
  schedule: "0 * * * *",
  workflow: pollinateProductsWorkflow,
});
```

The flow runtime writes runs into `khotan_runs`, and your workflow can keep durable snapshots or cursors in cache tables, so the same execution can be inspected later in the Hub, Runs table, or CLI.

## 5. Define a wire and webhook handlers

Use a **wire** when the source system supports webhook subscription registration. Use **catch** to process the event in your app. Use **pass** to forward it to another plug.

```ts
// src/khotan/wires/pollinate-wire.ts
import { wire } from "@/khotan/wires/wire";

export const pollinateWire = wire({
  events: ["order.created"],
  async onSubscribe(ctx) {
    const res = await ctx.plug.post<{ id: string; secret: string }>("/webhooks", {
      body: {
        url: ctx.callbackUrl,
        events: ctx.events,
      },
    });

    await ctx.setWireVars({ webhookSecret: res.secret });
    return { remoteId: res.id };
  },
  async onUnsubscribe(ctx) {
    await ctx.plug.delete(`/webhooks/${ctx.remoteId}`);
  },
  async onVerify(ctx) {
    return Boolean(ctx.headers["x-signature"] && ctx.wireVars["webhookSecret"]);
  },
});
```

```ts
// src/khotan/webhooks/pollinate-catch.ts
import { khotanCache } from "khotan-data/factory";
import { catchEvent, type CatchContext } from "@/khotan/webhooks/catch";

async function pollinateCatchWorkflow(ctx: CatchContext) {
  "use workflow";

  async function handleEvent() {
    "use step";
    const dedupeCache = khotanCache(ctx, "pollinate-webhook-markers");
    const eventId = String(ctx.event["id"] ?? "");
    if (eventId && (await dedupeCache.get<boolean>(eventId))) return;

    console.log("Handled event", ctx.eventType, ctx.khotanRunId);

    if (eventId) {
      await dedupeCache.set(eventId, true);
    }
  }

  await handleEvent();
}

export const pollinateCatch = catchEvent({
  name: "pollinate-orders",
  events: ["order.created"],
  workflow: pollinateCatchWorkflow,
});
```

```ts
// src/khotan/webhooks/pollinate-to-slack.ts
import { pass, type PassContext } from "@/khotan/webhooks/pass";

async function pollinateToSlackWorkflow(ctx: PassContext) {
  "use workflow";

  async function forwardEvent() {
    "use step";
    console.log("Forwarding event", ctx.eventType, ctx.destVars);
  }

  await forwardEvent();
}

export const pollinateToSlack = pass({
  name: "pollinate-to-slack",
  to: "slack",
  events: ["order.created"],
  workflow: pollinateToSlackWorkflow,
});
```

## 6. Register everything in the factory config

The factory config is where khotan comes together:

```ts
// src/khotan/khotan.ts
import { khotan, drizzleAdapter } from "khotan-data/factory";
import { db } from "@/db";
import { pollinatePlug } from "./plugs/pollinate";
import { slackPlug } from "./plugs/slack";
import { pollinateProductsSnapshotCache } from "./caches/pollinate-products-snapshot";
import { pollinateProductsInflow } from "./flows/pollinate-products";
import { pollinateWire } from "./wires/pollinate-wire";
import { pollinateCatch } from "./webhooks/pollinate-catch";
import { pollinateToSlack } from "./webhooks/pollinate-to-slack";

const khotanData = khotan({
  adapter: drizzleAdapter(db),
  resources: [
    {
      name: "products",
      mapping: {
        connectField: "sku",
        plugs: {
          pollinate: { uniqueIdentifier: "id" },
        },
      },
      description: "Shared product identity",
    },
    {
      name: "customers",
      mapping: {
        connectField: ["tenantId", "email"],
        plugs: {
          pollinate: { uniqueIdentifier: "id" },
          slack: { uniqueIdentifier: "email" },
        },
      },
      description: "Shared customer identity across systems",
    },
  ],
  caches: [
    pollinateProductsSnapshotCache,
  ],
  plugs: [
    {
      name: "pollinate",
      plug: pollinatePlug,
      flows: [pollinateProductsInflow],
      wires: [pollinateWire],
      catches: [pollinateCatch],
      passes: [pollinateToSlack],
    },
    {
      name: "slack",
      plug: slackPlug,
    },
  ],
});

export default khotanData;
```

On the first real runtime request, khotan upserts the registered plugs, caches, flows, resources, and webhook handlers into its tables.

When a resource declares mapping participants, khotan also uses that contract to validate mapping mutations and to derive one canonical `connectValue` for lookup and browsing.

## 7. Operate the runtime

Once the app is running, you can use both UI and CLI surfaces:

```terminal
$ npx khotan plug --list
$ npx khotan plug pollinate --info
$ npx khotan wire pollinate connect
$ npx khotan flows list
$ npx khotan flows trigger pollinate-products --plug pollinate --variant full
$ npx khotan mappings list customers --search "alice@example.com"
$ npx khotan mappings lookup customers --connect-value "alice@example.com"
```

And if you installed the dashboard components:

```terminal
$ npx khotan add config-page-1
```

Visit `/config` to see the Hub, toggles, and manual triggers. Add the Runs component or `logs-page-1` block if you want a dedicated execution log surface.

Visit `/mappings` if you scaffolded `mappings-page-1`. The browser lets you search one resource at a time, edit per-plug refs, and keep contextual metadata separate from identity fields.

## 8. Useful mental model

- A **plug** talks to an external API.
- A **cache** stores durable workflow state between runs.
- A **flow** moves data on a schedule or on demand.
- A **wire** registers a callback URL with the source service.
- A **catch** handles the verified inbound event.
- A **pass** forwards a verified inbound event to another plug.
- A **resource** defines the shared entity contract used for mappings.
- A **mapping** stores one canonical shared identity plus per-plug refs.
- A **run** records every durable execution in a consistent table.

## Next steps

- Read [Configuration](/docs/configuration) for the factory shape and env vars.
- Read [CLI Reference](/docs/cli) for `plug`, `wire`, `flows`, and `mappings`.
- Read [Agent Guide](/docs/agents) if an agent will be helping with khotan work.
