Plugin System
Plugins are the mechanism by which infinite capabilities are added without touching the core platform. Every external integration — data source, communication channel, custom rule operator — is a plugin.
Three Plugin Types
1. Connector Plugins
Bridge external data sources to the platform. Pull data, transform it, store it.
interface Connector {
id: string; // e.g. "com.microsoft.graph"
name: string;
version: string;
// Called once on init — receives config and PluginContext
init(context: PluginContext): Promise<void>;
// Pull data since a given date
sync(options: SyncOptions): AsyncGenerator<RawItem>;
// Map raw items to unified model entities
mapToUnifiedModel(item: RawItem): Entity[];
// Optional: register for real-time push (webhooks)
registerRealtimeListener?(webhookUrl: string): Promise<void>;
// Optional: bidirectional — take actions on the external service
executeAction?(action: ConnectorAction): Promise<ActionResult>;
stop(): Promise<void>;
}
2. Channel Plugins
Handle bidirectional communication. Send and receive messages, calls, faxes.
interface Channel {
id: string; // e.g. "io.twilio.sms"
name: string;
version: string;
type: 'sms' | 'voice' | 'video' | 'email' | 'whatsapp' | 'fax' | string;
init(context: PluginContext): Promise<void>;
// Handle incoming event from external service (called by webhook handler)
handleIncomingEvent(payload: unknown): Promise<void>;
// Send outbound message
sendMessage(message: OutboundMessage): Promise<void>;
// Optional capabilities
placeCall?(call: OutboundCall): Promise<CallSession>;
sendFax?(fax: OutboundFax): Promise<void>;
generateAccessToken?(userId: string): Promise<string>; // For WebRTC clients
stop(): Promise<void>;
}
3. Rule Operator Plugins
Extend the Rules Engine with custom conditions and actions.
interface ConditionOperator {
id: string; // e.g. "location.IsUserAtVenue"
evaluate(params: Record<string, unknown>, context: RuleContext): Promise<boolean>;
}
interface ActionOperator {
id: string; // e.g. "smart-home.SetLightingScene"
execute(params: Record<string, unknown>, context: RuleContext): Promise<void>;
}
Plugin Manifest (plugin.json)
Every plugin must have a manifest declaring its identity and required permissions. The Plugin Manager validates this before loading. The shape below matches the real plugin.json files under apps/hub/plugins/ (flat permissions array + JSON-Schema configSchema). Note that packages/plugin-sdk/src/types.ts currently exports a stricter PluginManifest interface with a structured permissions: { network, data } object and a config: Array<...> field — the SDK types are ahead of the on-disk manifests and will converge in a future release.
{
"id": "com.microsoft.graph",
"name": "Microsoft Graph Connector",
"version": "1.0.0",
"type": "connector",
"entrypoint": "./index.ts",
"permissions": [
"network:outbound:graph.microsoft.com",
"event:publish:connector.*",
"event:publish:data.*"
],
"configSchema": {
"type": "object",
"properties": {
"clientId": { "type": "string" },
"tenantId": { "type": "string" }
},
"required": ["clientId", "tenantId"]
}
}
Permission Namespaces
| Permission | Grants |
|---|---|
network:outbound:<domain> | HTTPS requests to specific domain only |
network:inbound | Receive webhook calls |
filesystem:read:<path> | Read files under a path (Windows agent only) |
event:publish:<domain>.* | Publish events under a domain |
event:subscribe:<domain>.* | Subscribe to events under a domain |
data:read:<entityType> | Query entities via Data Service |
data:write:<entityType> | Write entities via Data Service |
storage:read | Read files from Supabase Storage |
storage:write | Write files to Supabase Storage |
V8 Sandboxing (isolated-vm)
Each plugin runs in a completely isolated V8 isolate via the isolated-vm package. This is meaningfully different from Node.js worker_threads:
isolated-vm | worker_threads | |
|---|---|---|
| Memory isolation | Complete — separate heap | Shared process heap |
| CPU limits | Enforced (wall time + CPU time) | Not enforced |
| Memory limits | Enforced (max heap size) | Not enforced |
| Security | No access to host globals | Can access shared Buffer, etc. |
PluginContext API
The PluginContext is the only interface a plugin has to the outside world. It contains exactly the functions the plugin's declared permissions grant.
// Source: packages/plugin-sdk/src/types.ts
interface PluginContext {
// Resolved config values (secrets pre-fetched from Supabase Vault)
config: Record<string, string | boolean | number>;
// Structured logger — writes to the hub log stream
logger: {
info(msg: string, data?: unknown): void;
warn(msg: string, data?: unknown): void;
error(msg: string, data?: unknown): void;
};
// Publish an immutable event to the NATS JetStream event bus
emitEvent(subject: string, data: unknown): Promise<void>;
// Read-only access to the unified data model
data: {
query<T>(table: string, filter: Record<string, unknown>): Promise<T[]>;
getById<T>(table: string, id: string): Promise<T | null>;
};
// Sandboxed HTTP client — only outbound hosts declared in
// manifest.permissions.network are allowed
http: {
get(url: string, headers?: Record<string, string>): Promise<unknown>;
post(url: string, body: unknown, headers?: Record<string, string>): Promise<unknown>;
};
}
Note: the SDK does not currently expose a
storageclient onPluginContext. Plugins that need storage must emit events and let a core service perform the write.
A plugin cannot do anything not exposed through PluginContext. It cannot import Node.js modules. It cannot call fetch directly. It cannot read process.env. The sandbox enforces this at the V8 level.
Plugin Lifecycle
Discovery → Validation → Installation → Loading → Sandboxing → Configuration → Running → Health Monitoring
│ │ │ │ │ │ │ │
scan validate install load code create inject call monitor
plugins/ manifest deps in into VM isolate PluginContext init() + restart
directory against isolated with validated start() on crash
schema env config
Communications Rule Operators
The Deep Communications integration ships a comms rule-operator plugin that exposes condition and action operators for all communication channels. These operators are used by the Rules Engine to build communication-aware automations.
Condition Operators
| Operator ID | Parameters | Description |
|---|---|---|
comms.callCount | contactId, direction (inbound|outbound|any), windowDays | True when the call count with a contact exceeds a threshold within a rolling time window |
comms.lastContact | contactId, channelType (voice|sms|email|any), withinDays | True when the last interaction with a contact was within N days |
comms.hasVoicemail | contactId? | True when there is at least one unlistened voicemail (optionally from a specific contact) |
comms.missedCalls | contactId?, count | True when there are N or more missed calls (optionally from a specific contact) |
Example rule using comms conditions:
# "If a VIP misses 3+ calls, send them a follow-up email"
name: VIP Missed Call Follow-up
trigger:
event: comms.CallMissed
conditions:
- operator: comms.missedCalls
params:
contactId: "{{ event.fromContactId }}"
count: 3
- operator: contact.HasTag
params:
tag: "vip"
actions:
- operator: comms.sendEmail
params:
to: "{{ event.fromContactId }}"
subject: "Sorry we missed you"
body: "We noticed we've missed your last few calls. Please reply to this email or call us back at your convenience."
Action Operators
| Operator ID | Parameters | Description |
|---|---|---|
comms.sendSms | to (contactId or E.164 number), body | Sends an SMS via the active Twilio channel plugin |
comms.sendEmail | to (contactId or email address), subject, body, replyToMessageId? | Sends an email via MS Graph on behalf of the authenticated user |
comms.createFollowUp | contactId, dueAt, note? | Creates a task in the unified model flagged as a communication follow-up |
Example rule using comms actions:
# "When I arrive at the office, send my pending follow-ups"
name: Office Arrival Follow-up Flush
trigger:
event: location.PlaceArrived
conditions:
- operator: location.IsUserAtPlace
params:
placeType: work
actions:
- operator: comms.sendSms
params:
to: "{{ followUp.contactId }}"
body: "Hey! I'm now at the office — happy to connect. Give me a call or reply here."
Registering the Comms Rule-Operator Plugin
// apps/hub/plugins/rule-operators/comms/plugin.json
{
"id": "io.madeopen.comms-rules",
"name": "Communications Rule Operators",
"version": "1.0.0",
"type": "operator",
"entrypoint": "./index.ts",
"permissions": [
"data:read:conversations",
"data:read:messages",
"data:read:tasks",
"data:write:tasks",
"event:publish:comms.*"
]
}
Plugin Locations
apps/hub/plugins/
├── connectors/ 8 connector plugins
│ ├── ms-graph/ Microsoft Graph (contacts, calendar, email)
│ ├── twilio/ Twilio (logs historical calls/SMS)
│ ├── fitbit/ Fitbit health + activity
│ ├── google-fit/ Google Fit health + activity
│ ├── plaid/ Plaid financial accounts + transactions
│ ├── nutritionix/ Nutritionix food database
│ ├── usda-food/ USDA FoodData Central
│ └── device/ Device connector (Android/Windows sensor events)
└── channels/ 2 channel plugins
├── twilio/ Twilio Channel (real-time voice/SMS)
└── whatsapp/ WhatsApp Cloud API (Meta Graph)
Each directory contains a plugin.json manifest plus an index.ts entry file.
Total: 10 plugins (8 connectors + 2 channels).
Note: Twilio appears twice — once as a connector (syncing historical call/SMS data) and once as a channel (handling real-time inbound/outbound communication). This is the correct pattern per the architecture.
The communications rule-operators described above are not currently shipped as a standalone plugin directory under apps/hub/plugins/rule-operators/; they are documented here as the intended surface once rule-operator plugins land.