Skip to content
API Blog

Reactions

API reference for the reactions surface of @lobu/connector-sdk. Reactions are TypeScript files that run after a watcher’s extraction lands; for a tutorial-style introduction see the Reactions guide.

All reaction types live in @lobu/connector-sdk — there is no separate @lobu/reaction-sdk package on npm. Import them by name:

import type { ReactionContext, ReactionEntity } from "@lobu/connector-sdk";

The matching client runtime is injected by the Lobu sandbox at execution time. It is not importable — its shape lives in packages/server/src/sandbox/client-sdk.ts and only the context types are shared across packages.


A reaction file default-exports an async function:

import type { ReactionContext } from "@lobu/connector-sdk";
// Declare the subset of the injected ClientSDK your reaction touches.
// `@lobu/connector-sdk` doesn't export `ClientSDK` (the implementation
// lives in the server package), so pin only what you call.
interface ReactionClient {
knowledge: {
save(input: {
entity_ids?: number[];
content: string;
semantic_type: string;
title?: string;
metadata?: Record<string, unknown>;
}): Promise<unknown>;
};
}
export default async (
ctx: ReactionContext,
client: ReactionClient,
params?: Record<string, unknown>,
): Promise<void> => {
// …
};
ArgumentDescription
ctxThe watcher-window context — extraction output, attached entities, window metadata.
clientThe ClientSDK instance injected by the sandbox. Use client.knowledge.* for memory reads/writes; use fetch for outbound HTTP.
paramsOptional bag of reaction-specific parameters (rare — most reactions ignore this).

Throwing fails the reaction run; the error is surfaced to the watcher run log. Returning void is success — there is no need to return the saved-event ID.


interface ReactionContext {
/** The extracted analysis data from the completed window */
extracted_data: Record<string, unknown>;
/** All entities the watcher is attached to */
entities: ReactionEntity[];
/** The window that was just completed */
window: {
id: number;
watcher_id: number;
window_start: string;
window_end: string;
granularity: string;
content_analyzed: number;
};
/** Watcher identity */
watcher: {
id: number;
slug: string;
name: string;
version: number;
};
/** Organization context */
organization_id: string;
}
FieldNotes
extracted_dataThe LLM’s output, already validated against the watcher’s extraction_schema. Cast to a concrete interface — TypeScript can’t infer it for you, since the schema is YAML-defined.
entitiesEvery entity the watcher is attached to. Common pattern: entity_ids: ctx.entities.map((e) => e.id) when calling client.knowledge.save.
windowwindow_start / window_end are ISO strings; granularity matches the watcher’s schedule (1h, 1d, …).
watcherslug is stable across version bumps — use it for grep-friendly log lines.
organization_idOrg UUID. Forward to external systems that need explicit org-scoping.

interface ReactionEntity {
id: number;
name: string;
entity_type: string;
metadata: Record<string, unknown>;
}

Each entity carries the org-scoped numeric id (use for entity_ids on save), the display name, the type slug (Company, Project, $member), and any metadata traits accreted by connector ingestion or earlier watchers.


Not exported from @lobu/connector-sdk — injected as the second argument at runtime. The shape lives in packages/server/src/sandbox/client-sdk.ts. Below is the subset reactions reach for in practice.

MethodUse
save({ entity_ids?, content, semantic_type, title?, slug?, metadata? })Append a new event to memory.
search({ query?, entity_type?, entity_id?, limit?, ... })Hybrid (vector + full-text) search across the org’s events. Use to dedupe before writing.
read({ content_id? | watcher_id?, entity_ids?, since?, until?, limit? })Fetch a single event by id, or pull events from a watcher window.
delete(event_id) or delete({ event_id?, event_ids?, reason? })Append a tombstone for one or more events. events is append-only — delete writes a superseding row, never DELETEs.

Reactions hit external systems (Slack incoming webhooks, Linear, GitHub) directly with fetch. The worker proxy enforces the same WORKER_ALLOWED_DOMAINS policy as connector code, so non-allowlisted hosts are blocked at the network layer — no extra wrapper required.

When you need to call a third-party API that an installed connector already authenticates, fetch the token through the gateway proxy instead of duplicating credentials in the reaction.


  1. Watcher window closes. The watcher’s prompt + extraction_schema runs against the events in the window; the extracted JSON is validated.
  2. Lobu runs the watcher’s reaction. The watcher’s reaction script (the .ts file referenced by defineWatcher({ reaction: reactionFromFile("./account-health-monitor.reaction.ts") })) runs. If the watcher declares no reaction, the run ends here.
  3. Sandbox boots the reaction. Isolated worker, network restricted by the agent’s WORKER_ALLOWED_DOMAINS, stdout/stderr captured into the run record, hard timeout.
  4. Reaction runs. Any client.knowledge.save calls append events; outbound fetch calls go through the worker HTTP proxy.
  5. Result lands. Success or failure is recorded on the watcher run; partial side effects (events already saved before a throw) stay in place — they’re real events in the durable log.