Skip to content
API Blog

@lobu/connector-sdk

@lobu/connector-sdk contains two authoring surfaces:

  • Connectors ingest or query external systems and expose governed operations.
  • Reactions run deterministic TypeScript after an automation produces validated output.

Start with the Connector guide if you want a complete example.

Every symbol below is a re-export from the package’s top-level entry point:

import {
ConnectorRuntime,
type ConnectorDefinition,
type SyncContext,
type SyncResult,
type EventEnvelope,
type ActionContext,
type ActionResult,
type ReactionContext,
type ReactionClient,
} from "@lobu/connector-sdk";

Abstract base class. Every connector extends it.

abstract class ConnectorRuntime<C = Record<string, unknown>, F = Record<string, unknown>> {
abstract readonly definition: ConnectorDefinition;
abstract sync(ctx: SyncContext<C, F>): Promise<SyncResult<C>>;
async execute(ctx: ActionContext): Promise<ActionResult>; // default: { success: false, error: "Actions not supported" }
async query(ctx: QueryContext<F>): Promise<QueryResult>; // default: throws
async search(ctx: SearchContext<F>): Promise<QueryResult>; // default: throws
async registerWebhook(ctx: WebhookRegistrationContext<F>): Promise<WebhookRegistration>; // default: throws
async unregisterWebhook(ctx: WebhookRegistrationContext<F>): Promise<void>; // default: no-op
async reflectMetrics(ctx: ReflectContext<F>): Promise<ReflectResult>; // default: []
async authenticate(ctx: AuthContext): Promise<AuthResult>; // default: throws
}
MemberRequiredDescription
definitionyesStatic metadata: key, name, version, auth, feeds, actions. See ConnectorDefinition.
sync(ctx)yesPull data. Receives last checkpoint, returns events + new checkpoint.
execute(ctx)noRun an action. Default returns { success: false, error: "Actions not supported" } — override only when actions are declared.
query(ctx)noRun a live read-only query without persisting events. Used for virtual-feed reads. Default throws.
search(ctx)noRun live keyword recall against a virtual feed. Default throws.
registerWebhook(ctx)noSubscribe a connection to provider webhooks. Required for registered webhook delivery.
unregisterWebhook(ctx)noRemove the provider subscription when a connection is deleted. Default is a no-op.
reflectMetrics(ctx)noFederate entity types from the source’s native semantic layer (e.g. Snowflake). Default returns [].
authenticate(ctx)noOnly required when authSchema.methods includes { type: "interactive" }. Stream AuthArtifacts, await UI signals.

IntegrationConnector is the base for kind: "integration" definitions with no feeds. BridgeOnlyConnector is the base for definitions whose sync and action handlers live on an authorized device rather than the server worker.


interface ConnectorDefinition {
key: string; // 'google.gmail'
name: string;
description?: string;
version: string; // semver
kind?: "data" | "integration"; // default: data
authSchema?: ConnectorAuthSchema;
feeds?: Record<string, FeedDefinition>;
actions?: Record<string, ActionDefinition>;
automationEvents?: ConnectorAutomationEvent[];
webhook?: ConnectorWebhookSchema;
optionsSchema?: Record<string, unknown>; // JSON Schema for global options
faviconDomain?: string; // e.g. 'x.com'
mcpConfig?: { upstreamUrl: string }; // proxy to an upstream MCP server
openapiConfig?: { // auto-generate from OpenAPI
specUrl: string;
includeOperations?: string[];
excludeOperations?: string[];
includeTags?: string[];
serverUrl?: string;
};
requiredCapability?: string; // e.g. 'screentime' (device-only)
runtime?: ConnectorRuntimeInfo; // pin to a device platform
agentTooling?: ConnectorAgentTooling; // CLI, env, and domains contributed to agents
}

Use kind: "integration" for app installations that receive inbound chat or webhook traffic but expose no syncable feeds. Data connectors can combine polled feeds, virtual feeds, webhooks, and actions.

interface ConnectorRuntimeInfo {
platforms: Array<
"ios" | "android" | "macos" | "windows" | "linux" | "chrome-extension"
>;
scopes?: string[]; // forwarded to the native platform adapter
nix?: { packages: string[] }; // native deps provisioned at run time
}

platforms and scopes describe device-bound connectors; omit the runtime block for cloud-fleet connectors that do not pin to a device.

nix.packages lists native system dependencies as nixpkgs attribute refs (for example ["ffmpeg", "imagemagick"]) the connector needs on PATH at execution time. npm dependencies are bundled into the connector at compile time and do not go here. Backends that can run native deps (embedded, container, machine) provision them via nix-shell; backends that cannot (for example edge workers) reject a connector that declares them. See Dependencies in the guide for the npm-vs-native split.

Use agentTooling when an installed connection should contribute a native CLI to the agent workspace:

interface ConnectorAgentTooling {
nix?: { packages: string[] };
env?: Array<{
name: string;
credential: "lease";
}>;
domains?: string[];
}

This is separate from runtime.nix: runtime provisions the connector worker; agentTooling contributes packages and network access to the agent that uses the connection. Only short-lived leased credentials can be injected into agent tooling.

The connector-level webhook block tells the gateway how to verify, deduplicate, and route inbound deliveries. Registered delivery uses a per-connection callback created by registerWebhook. App-installation delivery uses a shared provider endpoint and gateway-managed installation credentials.


interface ConnectorAuthSchema {
methods: ConnectorAuthMethod[];
}
type ConnectorAuthMethod =
| ConnectorAuthNone
| ConnectorAuthEnvKeys
| ConnectorAuthOAuth
| ConnectorAuthBrowser
| ConnectorAuthInteractive
| ConnectorAuthAppInstallation;
{ type: "none" }
interface ConnectorAuthEnvKeys {
type: "env_keys";
required?: boolean;
scope?: "connection" | "organization";
fields: ConnectorAuthEnvField[];
description?: string;
}
interface ConnectorAuthEnvField {
key: string;
label?: string;
description?: string;
example?: string;
secret?: boolean;
required?: boolean;
}
interface ConnectorAuthOAuth {
type: "oauth";
provider: string;
requiredScopes: string[];
optionalScopes?: string[];
required?: boolean;
description?: string;
scope?: "connection" | "organization";
authorizationUrl?: string;
tokenUrl?: string;
userinfoUrl?: string;
authParams?: Record<string, string>;
tokenEndpointAuthMethod?: "client_secret_post" | "client_secret_basic" | "none";
usePkce?: boolean;
loginScopes?: string[];
clientIdKey?: string;
clientSecretKey?: string;
setupInstructions?: string;
loginProvisioning?: { autoCreateConnection?: boolean };
}
interface ConnectorAuthBrowser {
type: "browser";
required?: boolean;
description?: string;
capture?: "cli" | "cdp"; // cli = lobu memory browser-auth; cdp = remote debug
requiredDomains?: string[];
defaultCdpUrl?: string; // default: http://127.0.0.1:9222
}
interface ConnectorAuthInteractive {
type: "interactive";
required?: boolean;
description?: string;
scope?: "connection" | "organization";
expectedArtifact?: "qr" | "code" | "redirect" | "prompt" | "status";
timeoutSec?: number; // default 300
}

Connectors declaring this method must implement authenticate(ctx) and stream AuthArtifacts.

Use app_installation for organization- or workspace-scoped apps such as GitHub Apps, Slack apps, or Jira site installations. The gateway owns the installation handshake, stores the tenant identity, verifies shared webhook deliveries, and mints provider credentials.

interface ConnectorAuthAppInstallation {
type: "app_installation";
provider: string;
installShape?: "oauth-code-exchange" | "github-app";
authorizeUrl?: string;
tokenUrl?: string;
providerInstance?: string;
appIdKey?: string;
privateKeyKey?: string;
appSlugKey?: string;
clientIdKey?: string;
clientSecretKey?: string;
webhookSecretKey?: string;
installUrlTemplate?: string;
permissions?: string[];
events?: string[];
required?: boolean;
description?: string;
}

interface FeedDefinition {
key: string;
name: string;
description?: string;
requiredScopes?: string[]; // OAuth scopes that must be held
displayNameTemplate?: string; // "{subreddit} - {content_type}"
configSchema?: Record<string, unknown>; // JSON Schema for per-feed config
userManaged?: boolean; // skip auto-wire creation
virtual?: boolean; // query live; do not sync or persist
webhook?: {
events: string[];
mode?: "trigger" | "store";
};
eventKinds?: Record<string, {
description?: string;
metadataSchema?: Record<string, unknown>;
attributions?: EventAttributionRule[];
}>;
}

Virtual feeds implement query(ctx) and optionally search(ctx). A webhook feed can either schedule a canonical sync (trigger) or store a complete delivery directly (store).

Attribution connects each event to the people, accounts, or resources it is about.

interface EventAttributionRule {
role: "authored_by" | "about" | "mentions" | "belongs_to";
target: {
entityType?: string;
identities?: EntityIdentitySpec[];
titlePath?: string;
createWhen?: EntityLinkPredicate;
};
autoCreate?: boolean;
traits?: Record<string, EntityTraitSpec>;
}
interface EntityIdentitySpec {
namespace: string; // 'phone', 'email', or custom
eventPath: string; // dot path into the event
matchOnly?: boolean;
primary?: boolean;
}
interface EntityTraitSpec {
eventPath: string;
mergeStrategy: "init_only" | "prefer_non_empty" | "overwrite";
}

Canonical namespaces for cross-connector identity. Use these when targeting $member so identities align automatically:

const IDENTITY = {
PHONE: "phone",
EMAIL: "email",
EMAIL_DOMAIN: "email_domain",
AUTH_USER_ID: "auth_user_id",
} as const;
type IdentityNamespace = (typeof IDENTITY)[keyof typeof IDENTITY];

IDENTITY contains provider-independent namespaces. Connector-specific namespaces such as github_user_id or slack_user_id are exported by their connector package.


interface ActionDefinition {
key: string;
name: string;
description?: string;
requiresApproval: boolean;
kind?: "read" | "write"; // default: write
requiredScopes?: string[];
annotations?: {
readOnlyHint?: boolean;
destructiveHint?: boolean;
openWorldHint?: boolean;
idempotentHint?: boolean;
};
inputSchema?: Record<string, unknown>; // JSON Schema
outputSchema?: Record<string, unknown>;
}

interface SyncContext<C = Record<string, unknown>, F = Record<string, unknown>> {
feedKey: string;
feedId?: number | null; // stable id of this feed instance; undefined for direct/programmatic calls
config: F;
checkpoint: C | null;
credentials: SyncCredentials | null;
entityIds: number[];
sessionState?: Record<string, unknown> | null;
installation?: ConnectorInstallationContext;
emitEvents?: (events: EventEnvelope[]) => Promise<void>;
updateCheckpoint?: (checkpoint: C | null) => Promise<void>;
}
interface SyncCredentials {
provider: string;
accessToken: string;
refreshToken?: string | null;
expiresAt?: string | null;
scope?: string | null;
}

emitEvents and updateCheckpoint are present only on the long-running sync path. For short syncs return the full result and ignore both hooks.

Where credentials land. ctx.credentials is populated for oauth auth. For env_keys, the values are merged into ctx.config under the keys declared on each ConnectorAuthEnvField. For browser auth, captured cookies arrive on ctx.sessionState. The same rules apply to ActionContext.


interface SyncResult<C = Record<string, unknown>> {
events: EventEnvelope[];
checkpoint: C | null;
auth_update?: Record<string, unknown> | null;
metadata?: {
items_found?: number;
items_skipped?: number;
[key: string]: unknown;
};
}

auth_update is for connectors whose credentials rotate during sync (browser cookies, opaque session tokens). The gateway persists it back onto the linked auth profile.


Virtual feeds use query(ctx) for structured reads and search(ctx) for keyword recall. Results are returned to the caller and are not written to the event log.

interface QueryContext<F = Record<string, unknown>> {
feedKey?: string;
query: string;
config: F;
credentials: SyncCredentials | null;
sessionState?: Record<string, unknown> | null;
installation?: ConnectorInstallationContext;
limit?: number;
offset?: number;
sort?: { column: string; order: "asc" | "desc" };
}
interface SearchContext<F = Record<string, unknown>>
extends QueryContext<F> {
terms: string[];
}
interface QueryResult {
rows: Record<string, unknown>[];
columns?: Array<{ name: string; type: string }>;
total?: number;
}

The durable event shape returned by connector syncs.

interface EventEnvelope {
origin_id: string; // required: platform's unique ID
origin_type?: string; // source-native item type
payload_type?: "text" | "markdown" | "json_template" | "media" | "empty";
payload_text: string; // required: main content
payload_data?: Record<string, unknown>; // structured data
payload_template?: Record<string, unknown> | null;
attachments?: unknown[];
title?: string;
author_name?: string;
source_url?: string;
occurred_at: Date; // required: when it happened
semantic_type?: string; // content/note/summary/fact/…
score?: number; // 0-100 engagement
origin_parent_id?: string; // hierarchical content
metadata?: Record<string, unknown>;
embedding?: number[]; // pre-computed
automation_signals?: ConnectorAutomationSignalDraft[];
}

interface ActionContext {
actionKey: string;
input: Record<string, unknown>;
credentials: SyncCredentials | null;
config: Record<string, unknown>;
sessionState?: Record<string, unknown> | null;
installation?: ConnectorInstallationContext;
}
interface ActionResult {
success: boolean;
output?: Record<string, unknown>;
error?: string;
}

For connectors with { type: "interactive" } auth.

interface AuthContext {
config: Record<string, unknown>;
previousCredentials: Record<string, unknown> | null;
emit: (artifact: AuthArtifact) => Promise<void>;
awaitSignal: (
name: string,
options?: { timeoutMs?: number }
) => Promise<Record<string, unknown>>;
signal: AbortSignal;
}
type AuthArtifact =
| { type: "qr"; value: string; expiresAt?: string; instructions?: string }
| { type: "code"; value: string; expiresAt?: string; instructions?: string }
| {
type: "redirect";
url: string;
mode: "popup" | "same-tab";
awaitSignal: string;
instructions?: string;
}
| {
type: "prompt";
fields: Array<{
key: string;
label: string;
kind: "text" | "password" | "otp";
required?: boolean;
}>;
submitSignal: string;
instructions?: string;
}
| { type: "status"; message: string };
interface AuthResult {
credentials: Record<string, unknown>;
metadata?: {
account_id?: string;
display_name?: string;
expires_at?: string;
[key: string]: unknown;
};
}

ctx.emit(artifact) streams the next thing the UI should show. Each emit replaces the previously active artifact. Pause on ctx.awaitSignal("name") until the UI POSTs the matching signal.


Read-only records passed to runtime APIs that inspect an existing connection, feed, or run.

interface Connection {
id: number;
organizationId: string;
connectorKey: string;
displayName?: string;
status: "active" | "paused" | "error" | "revoked";
accountId?: string;
credentials?: Record<string, unknown>;
entityIds?: number[];
config?: Record<string, unknown>;
errorMessage?: string;
createdBy?: string;
createdAt: Date;
updatedAt: Date;
}
interface Feed {
id: number;
organizationId: string;
connectionId: number;
feedKey: string;
status: "active" | "paused" | "error";
entityIds?: number[];
config?: Record<string, unknown>;
checkpoint?: Record<string, unknown>;
syncIntervalMs?: number;
nextSyncAt?: Date;
lastSyncAt?: Date;
lastSyncStatus?: string;
lastError?: string;
consecutiveFailures: number;
itemsCollected: number;
createdAt: Date;
updatedAt: Date;
}
type RunType = "sync" | "action" | "code" | "automation" | "auth";
type RunStatus = "pending" | "running" | "completed" | "failed" | "cancelled" | "timeout";
type ApprovalStatus = "pending" | "approved" | "rejected" | "expired" | "auto";
interface Run {
id: number;
organizationId: string;
runType: RunType;
feedId?: number;
connectionId?: number;
actionKey?: string;
actionInput?: Record<string, unknown>;
actionOutput?: Record<string, unknown>;
approvalStatus: ApprovalStatus;
status: RunStatus;
claimedBy?: string;
claimedAt?: Date;
lastHeartbeatAt?: Date;
completedAt?: Date;
connectorKey?: string;
connectorVersion?: string;
checkpoint?: Record<string, unknown>;
itemsCollected: number;
errorMessage?: string;
createdAt: Date;
}

The same package exports the public types for automation reactions:

import type {
KnowledgeSaveInput,
KnowledgeSaveResult,
ReactionClient,
ReactionContext,
} from "@lobu/connector-sdk";
export default async function react(
ctx: ReactionContext,
client: ReactionClient
): Promise<void> {
client.log("Reaction started", {
automation_id: ctx.automation.id,
window_id: ctx.window.id,
});
}

ReactionContext contains validated output, attached entities, window provenance, automation identity, and both the organization id and stable workspace slug. ReactionClient exposes knowledge, entities, notifications, operations, read-only query, and structured log.

knowledge.save(input) accepts a stable idempotency_key, optional automation_source, source and parent-event provenance, and returns KnowledgeSaveResult:

interface KnowledgeSaveResult {
id: number;
created: boolean;
metadata: Record<string, unknown>;
}

notifications.send(input) also accepts idempotency_key. Use the same key when retrying an effect. See Reactions for the complete contract and examples.


The package also re-exports a few utilities so connectors share one implementation:

ExportPurpose
ky, HTTPError, KyInstance, OptionsShared HTTP client.
withHttpRetry(fn, opts?)Retry-with-backoff wrapper for transient HTTP failures.
calculateEngagementScore(signals)Maps raw engagement metrics into a normalised 0-100 score.
Type, Static, ValueTypeBox helpers for connector config, action, and event schemas. Reactions use plain JSON Schema instead.
sdkLogger (alias logger)Connector-scoped logger; output is captured by the run record.
normalizeEmail, normalizeEmailDomain, normalizePhone, normalizeAuthUserId, normalizeIdentifierNormalizers for generic identity namespaces. Connector packages own provider-specific normalizers.
AUTOMATION_TIME_GRANULARITIES, alignToAutomationWindowStart, …Time helpers used by automation scheduling.
Browser SDK: acquireBrowser, launchBrowser, CdpPage, browserNetworkSync, extensionNetworkSync, etc.Headless / CDP / extension browser primitives for browser and cdp capture.

See the source on GitHub for the full helper surface.

Last updated: