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.
Reaction signature
Section titled “Reaction signature”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> => { // …};| Argument | Description |
|---|---|
ctx | The watcher-window context — extraction output, attached entities, window metadata. |
client | The ClientSDK instance injected by the sandbox. Use client.knowledge.* for memory reads/writes; use fetch for outbound HTTP. |
params | Optional 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.
ReactionContext
Section titled “ReactionContext”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;}| Field | Notes |
|---|---|
extracted_data | The 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. |
entities | Every entity the watcher is attached to. Common pattern: entity_ids: ctx.entities.map((e) => e.id) when calling client.knowledge.save. |
window | window_start / window_end are ISO strings; granularity matches the watcher’s schedule (1h, 1d, …). |
watcher | slug is stable across version bumps — use it for grep-friendly log lines. |
organization_id | Org UUID. Forward to external systems that need explicit org-scoping. |
ReactionEntity
Section titled “ReactionEntity”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.
The injected client
Section titled “The injected client”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.
client.knowledge
Section titled “client.knowledge”| Method | Use |
|---|---|
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. |
Outbound HTTP
Section titled “Outbound HTTP”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.
Lifecycle
Section titled “Lifecycle”- Watcher window closes. The watcher’s prompt +
extraction_schemaruns against the events in the window; the extracted JSON is validated. - Lobu runs the watcher’s reaction. The watcher’s
reactionscript (the.tsfile referenced bydefineWatcher({ reaction: reactionFromFile("./account-health-monitor.reaction.ts") })) runs. If the watcher declares noreaction, the run ends here. - Sandbox boots the reaction. Isolated worker, network restricted by the agent’s
WORKER_ALLOWED_DOMAINS, stdout/stderr captured into the run record, hard timeout. - Reaction runs. Any
client.knowledge.savecalls append events; outboundfetchcalls go through the worker HTTP proxy. - 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.
See also
Section titled “See also”- Reactions guide — when to reach for a reaction, where the file lives, real-world example.
@lobu/connector-sdkreference — the connector surface of the same package.- Memory — how events become entity memory.