KB LabsDocs

SDK Hooks

Last updated April 7, 2026


How plugin handlers talk to platform services — the hook model.

Runtime hooks are how plugin handlers reach platform services: the LLM, the cache, the vector store, logging, analytics, config. They're composables — pattern borrowed from React — that read from a global platform singleton and return typed service instances (or undefined when unconfigured).

This page is the plugin-facing overview. For the full hook API reference — every method, every parameter — see SDK → Hooks.

The one rule

Plugin handlers import hooks from @kb-labs/sdk only. Not from @kb-labs/shared-command-kit, not from @kb-labs/core-platform, not from anywhere else. The SDK re-exports every hook you can use; internal packages are off-limits.

TypeScript
import {
  usePlatform,
  useLogger,
  useLLM,
  useCache,
  useStorage,
  useConfig,
  useEmbeddings,
  useVectorStore,
  useAnalytics,
  useEventBus,
} from '@kb-labs/sdk';

The rule exists because @kb-labs/core-platform and friends evolve independently of the SDK. The SDK is the stable surface; everything behind it may change between minor versions.

Why hooks instead of ctx.platform.*

The platform context (ctx.platform) does expose the same services directly:

TypeScript
async execute(ctx, input) {
  const cache = ctx.platform.cache;      // ← works, but don't
  if (cache) await cache.set('k', 'v');
}

But you shouldn't use this path. Use the hooks:

TypeScript
import { useCache } from '@kb-labs/sdk';
 
async execute(ctx, input) {
  const cache = useCache();              // ← idiomatic
  if (cache) await cache.set('k', 'v');
}

Three reasons:

  1. Hooks are versioned through the SDK. ctx.platform.cache goes through @kb-labs/plugin-contracts, which is not what plugins should couple to. useCache() is in @kb-labs/sdk, which is.
  2. Hooks handle wrapping automatically. The platform wraps every adapter in analytics, tracing, and rate-limiting layers. useLLM() returns the fully wrapped adapter; ctx.platform.llm sometimes skips those wrappers depending on the runtime path.
  3. Hooks are testable without a full context. In tests, you can stub out the platform singleton without building a full PluginContextV3. See SDK → Testing.

The rule of thumb: when a hook exists for a service, use it. Reach for ctx.platform only for services without a dedicated hook (rare — the SDK exports hooks for every important service).

Availability: undefined means "not configured"

Most hooks return T | undefined:

TypeScript
const cache = useCache();
if (cache) {
  await cache.set('k', 'v', 60_000);
}

undefined means the adapter isn't in platform.adapters — the user hasn't configured it. Handlers should degrade gracefully:

TypeScript
async execute(ctx, input) {
  const cache = useCache();
  const cacheKey = `query:${input.id}`;
 
  if (cache) {
    const hit = await cache.get(cacheKey);
    if (hit) return { exitCode: 0, result: hit };
  }
 
  const result = await expensiveQuery(input.id);
 
  if (cache) {
    await cache.set(cacheKey, result, 300_000);
  }
 
  return { exitCode: 0, result };
}

The handler runs fine with or without a cache — when present, it's faster; when absent, it falls back to computing every time.

Exception: useLogger is always defined

The one hook that never returns undefined:

TypeScript
const logger = useLogger();
logger.info('task started', { taskId });   // always works

The platform guarantees a logger exists — even if nothing is configured, it falls back to a console logger. You don't need a null check.

The hook catalog

Quick reference. See SDK → Hooks for every method and option.

HookReturnsNullableUse for
usePlatform()full platform singletonnoaccessing services that don't have dedicated hooks
useLogger()ILoggernostructured logging
useLLM(options?)ILLMyestext generation, streaming, tool calls
useCache()ICacheyesKV, sorted sets, atomic ops
useStorage()IStorageyesdurable blob/file storage
useVectorStore()IVectorStoreyessemantic search, upsert, filter
useEmbeddings()IEmbeddingsyestext → vector conversion
useAnalytics()IAnalyticsyesevent tracking
useEventBus()IEventBusyespub/sub across plugins
useConfig<T>(section?)T | undefinedyesplugin config from kb.config.json

usePlatform() — when nothing else fits

Returns the full platform object if you need a service that doesn't have its own hook (rare):

TypeScript
import { usePlatform } from '@kb-labs/sdk';
 
const platform = usePlatform();
const customAdapter = platform.getAdapter?.('myCustomService');

Prefer the dedicated hooks when they exist. usePlatform is a fallback.

Hook composition

Hooks can call other hooks. A common pattern is using useConfig to read plugin config, then passing pieces to other hooks:

TypeScript
import { useConfig, useLLM, useCache } from '@kb-labs/sdk';
 
interface MindConfig {
  defaultTier: 'small' | 'medium' | 'large';
  cacheTtl: number;
}
 
async execute(ctx, input) {
  const config = await useConfig<MindConfig>();
  const llm = useLLM({ tier: config?.defaultTier ?? 'small' });
  const cache = useCache();
 
  if (!llm) {
    return { exitCode: 1, error: { message: 'LLM not configured' } };
  }
 
  const cacheKey = `mind:${input.query}`;
  if (cache) {
    const hit = await cache.get(cacheKey);
    if (hit) return { exitCode: 0, result: hit };
  }
 
  const answer = await llm.complete(input.query);
  const result = { answer: answer.text, model: llm.model };
 
  if (cache) {
    await cache.set(cacheKey, result, config?.cacheTtl ?? 60_000);
  }
 
  return { exitCode: 0, result };
}

Each hook handles its own concerns. The handler orchestrates.

Testing with hooks

Hooks read from the platform singleton. In tests, you don't need to build a full context — you can replace the singleton:

TypeScript
import { createTestContext } from '@kb-labs/sdk/testing';
import { describe, it, expect, vi } from 'vitest';
import handler from './my-handler';
 
describe('my-handler', () => {
  it('uses the cache when available', async () => {
    const mockCache = {
      get: vi.fn().mockResolvedValue(null),
      set: vi.fn().mockResolvedValue(undefined),
      // ... other ICache methods
    };
 
    const ctx = createTestContext({
      platform: { cache: mockCache },
    });
 
    await handler.execute(ctx, { id: '123' });
 
    expect(mockCache.get).toHaveBeenCalledWith('query:123');
    expect(mockCache.set).toHaveBeenCalled();
  });
});

createTestContext builds a PluginContextV3 with a platform that useCache() and friends will read from. See SDK → Testing for full patterns.

What about the Studio side?

The hooks on this page are for plugin handlers — the code that runs inside CLI commands, REST routes, workflow steps, webhook receivers. The Studio side has its own set of hooks (useData, useMutateData, useSSE, useWebSocket, useNavigation, ...) imported from @kb-labs/sdk/studio.

Don't mix them. Handler code never imports from @kb-labs/sdk/studio; Studio page code never imports from @kb-labs/sdk's runtime hooks. Each subpath serves a different host.

See Plugins → Studio Pages for the Studio-side hooks.

SDK Hooks — KB Labs Docs