Operations & Events
The Fabric catalog has two halves: Operations (things the platform can do) and Events (things the platform emits). Both halves are returned by GET /api/v1/fabric/catalog. This page is a reference for both - what each entry means, how write flags and permissions govern execution, and which event types are always available.
For the broader architecture (how the Negotiator runs Connections, the executor’s two-track model, templating), see The Fabric. For step-by-step Connection authoring, see Connections.
Operations
Section titled “Operations”An Operation is a callable capability. It has an id, a human title, a JSON Schema for its input params, a write flag, and optionally a permission and a feature key.
Operation fields
Section titled “Operation fields”| Field | Type | Purpose |
|---|---|---|
id | string | Dotted identifier - native: {module}.{verb}; plugin: plugin.{id}.{verb} |
title | string | Human label shown in the Fabric cockpit |
description | string | One-sentence explanation |
input_schema | JSON Schema | Shape of the params dict the caller supplies |
produces | string[] | Media types the operation outputs (empty = no payload) |
accepts | string[] | Media types the operation can consume as an input artifact from a prior step |
permission | string or null | FreeSDN permission the invoking user must hold; null only for native non-write sinks |
write | boolean | true means the operation mutates a live device - it must route through the staged-change pipeline |
feature | string or null | Staging pipeline feature key; required (and validated) when write is true |
tier | native or plugin | Trust tier - governs how the executor runs the op |
provider_id | string | Module id (native) or plugin id (plugin) |
ID format and invariants
Section titled “ID format and invariants”Operation IDs must match ^[a-z0-9][a-z0-9_-]*(\.[a-z0-9][a-z0-9_-]*)+$. The registration code enforces four fail-closed invariants at startup - any violation raises a ValueError and prevents the catalog from loading that module:
- A write operation with no
feature→ rejected. - A write operation with no
permission→ rejected (a permissionless write would run for any Connection author). - A plugin operation whose id does not start with
plugin.→ rejected. - A native operation using the reserved
plugin.namespace → rejected.
The write flag and what it means in practice
Section titled “The write flag and what it means in practice”When write is true, the executor never invokes the op directly. It calls AdapterStagingService.stage_change() and returns a {staged: true, change_id, feature} result. The live device is not touched until an operator explicitly signs off in Pending Changes.
When write is false, the op’s handler runs immediately and its output is available to the next step in the chain.
The accepts / produces contract
Section titled “The accepts / produces contract”When an event or step produces a media-type artifact (for example image/jpeg from a camera motion event), the artifact flows to any downstream step that declares accepts: ["image/jpeg"]. The matchmaking is done by GET /api/v1/fabric/connections/suggest?source_event=<type>, which annotates each compatible operation with match: "artifact".
The special sentinel value blob (MEDIA_BLOB) matches any binary type on either side. An operation that declares accepts: ["image/jpeg"] but receives no artifact in the chain fails with ARTIFACT_REQUIRED before staging - the pipeline catches the mismatch early rather than letting a change queue that would 400 at sign-off.
Native operations by module
Section titled “Native operations by module”Six modules expose native operations in the catalog today. The table below is authoritative - any additions show up in GET /api/v1/fabric/catalog.
Read operations (immediate execution)
Section titled “Read operations (immediate execution)”| Operation ID | Module | Permission | Output |
|---|---|---|---|
cameras.snapshot | cameras | cameras.view | image/jpeg artifact |
network.client.list | network | network.view | application/json |
voip.phone.live_status | voip | voip.view | application/json |
firewall.search_alerts | firewall | firewall.view_logs | application/json |
storage.health | storage | storage.view | application/json |
These ops execute synchronously when the Negotiator runs the Connection step. Their output is available via {{steps.N.output.*}} in downstream steps.
Write operations (staged, operator sign-off required)
Section titled “Write operations (staged, operator sign-off required)”| Operation ID | Module | Permission | Staging feature key | Accepts |
|---|---|---|---|---|
storage.store_blob | storage | storage.write | storage.store_blob | image/jpeg, blob |
hypervisor.vm.snapshot | hypervisor | hypervisor.manage_snapshots | proxmox.snapshot.create | - |
hypervisor.vm.start | hypervisor | hypervisor.manage_vms | proxmox.vm.start | - |
hypervisor.vm.stop | hypervisor | hypervisor.manage_vms | proxmox.vm.stop | - |
hypervisor.vm.shutdown | hypervisor | hypervisor.manage_vms | proxmox.vm.shutdown | - |
hypervisor.vm.reboot | hypervisor | hypervisor.manage_vms | proxmox.vm.reboot | - |
Hypervisor power verbs are declared as discrete operations (one per verb) because the staging pipeline’s feature key must be static - a single parameterised hypervisor.vm.power op could not route to different applier paths.
Three built-in sinks
Section titled “Three built-in sinks”The fabric.* namespace provides three always-available sinks. They require no module to be enabled and never touch a live device:
| Operation ID | Purpose | Accepts | permission |
|---|---|---|---|
fabric.notify | Multi-channel notification (email / Slack / in-app / webhook) | application/json | null (native non-write sink) |
fabric.log | Structured audit log line - no side effects | - | null |
fabric.webhook | Outbound HTTP call to an external URL (POST by default; PUT/PATCH/GET also supported) | application/json | null |
Because their permission is null, invoking these sinks via POST /api/v1/fabric/operations/{id}/invoke requires the caller to be an unscoped org-admin. Connections authored by an org-admin may include these steps freely.
fabric.webhook is excluded from the AI assistant’s tool projection by design - the AI can never auto-POST org data to an arbitrary URL. Only a human operator wires this step.
Invoking an operation directly
Section titled “Invoking an operation directly”You can call a native operation once over HTTP without authoring a Connection:
POST /api/v1/fabric/operations/network.client.list/invokeAuthorization: Bearer <token>Content-Type: application/json
{"params": {}}| Behaviour | Detail |
|---|---|
| Non-native op | Returns 501 |
Op with a permission | Caller must hold that permission (API key scope is honoured) |
| Permissionless native sink | Caller must be an unscoped org-admin |
| Write op | Returns {"staged": true, "change_id": "<uuid>", "feature": "..."} - no device is touched |
| Failed op | Transaction rolls back; no partial row |
Events (EventSpecs)
Section titled “Events (EventSpecs)”An EventSpec describes a thing the platform emits. A Connection’s source_event field must match an event type in the catalog (or a wildcard pattern - see Connections for pattern syntax).
EventSpec fields
Section titled “EventSpec fields”| Field | Type | Purpose |
|---|---|---|
event_type | string | Dotted identifier - native: {domain}.{entity}.{action}; plugin: plugin.{id}.{type} |
title | string | Human label |
description | string | When the event fires and what it carries |
payload_schema | JSON Schema | Shape of the trigger payload available via {{trigger.*}} |
produces | string[] | Media types of any artifact the event carries alongside its JSON payload (empty array = pure data event; for example ["image/jpeg"]). Same shape as Operation produces. |
tier | native or plugin | Trust tier |
provider_id | string | Module or plugin that emits the event |
Platform-wide built-in event sources
Section titled “Platform-wide built-in event sources”These seven EventSpecs are always available regardless of which modules are enabled. They cover the two most important lifecycle streams - staged writes and device state - plus the inbound external bridge.
Staged-change lifecycle (3 events)
Section titled “Staged-change lifecycle (3 events)”Every adapter write in FreeSDN passes through the staged-change pipeline. These events let you react to that pipeline in real time.
Payload fields: change_id, controller_id, feature, operation, target_id, vendor, status, site_id, applied_at, actor_id.
| Event type | When it fires |
|---|---|
controller.change.staged | A write landed in the pending-changes queue awaiting sign-off |
controller.change.applied | A staged write was successfully applied to the device |
controller.change.failed | A staged write failed at the device or applier |
The vendor field discriminates which adapter was involved - use it in a Connection condition to filter by vendor:
{ "source_event": "controller.change.applied", "conditions": { "logic": "and", "conditions": [ {"field": "vendor", "operator": "eq", "value": "omada"} ] }}Device lifecycle (3 events)
Section titled “Device lifecycle (3 events)”Payload fields: device_id, site_id, data.name, data.old_status, data.new_status, data.reason.
| Event type | When it fires |
|---|---|
device.status.changed | A managed device went online or offline |
device.discovered | A new device was discovered or adopted on a controller |
device.updated | A managed device’s record changed (rename, re-IP, firmware update, etc.) |
Filter to offline events only:
{ "source_event": "device.status.changed", "conditions": { "logic": "and", "conditions": [ {"field": "data.new_status", "operator": "eq", "value": "offline"} ] }}Inbound external bridge (1 event)
Section titled “Inbound external bridge (1 event)”| Event type | When it fires |
|---|---|
ingest.external | Emitted by POST /api/v1/fabric/ingest from any external system |
The event type is always ingest.external - an external caller cannot spoof a native event type. Route different external callbacks by adding a condition on the name field:
{ "source_event": "ingest.external", "conditions": { "logic": "and", "conditions": [ {"field": "name", "operator": "eq", "value": "my_callback"} ] }}See The Fabric for the full ingest endpoint reference including rate limits and body caps.
Module-declared event sources (selected)
Section titled “Module-declared event sources (selected)”Each module declares additional event sources via its get_emitted_events() hook. The table below covers the current set. Check GET /api/v1/fabric/catalog for the live list.
Cameras
Section titled “Cameras”| Event type | Artifact produced | Notes |
|---|---|---|
cameras.event.motion | image/jpeg | Motion detected; artifact is a snapshot handle |
cameras.event.person | image/jpeg | Person-class detection; artifact is a snapshot handle |
camera.status.online | - | Camera came online |
camera.status.offline | - | Camera went offline |
camera.snapshot.captured | image/jpeg | Snapshot captured on demand or by schedule |
Camera events that produce image/jpeg can be chained into storage.store_blob to persist the snapshot - the Negotiator copies the artifact from the transient broker to the durable store before staging the write, so the bytes survive until an operator signs off.
Network
Section titled “Network”| Event type | Notes |
|---|---|
network.vlan.created | A VLAN record was created on a controller |
network.vlan.updated | A VLAN record was updated |
network.vlan.deleted | A VLAN record was deleted |
network.wifi.created | A WiFi / SSID record was created |
network.wifi.updated | A WiFi / SSID record was updated |
network.wifi.deleted | A WiFi / SSID record was deleted |
Firewall
Section titled “Firewall”| Event type | Notes |
|---|---|
gateway.sync.completed | Gateway orchestration sync completed across controllers |
gateway.brain.offline | The gateway brain controller went unreachable |
Storage and backup
Section titled “Storage and backup”| Event type | Notes |
|---|---|
storage.pool.degraded | A ZFS pool dropped below healthy state |
storage.capacity.warning | Pool capacity crossed the warning threshold |
backup.validation.failed | A scheduled backup validation run failed |
| Event type | Notes |
|---|---|
ai.budget.warning | Per-org AI token budget crossed its warning threshold |
Listing the catalog via API
Section titled “Listing the catalog via API”The catalog endpoint returns everything in one call:
curl -s https://<freesdn>/api/v1/fabric/catalog \ -H "Authorization: Bearer $TOKEN" | jq '{ op_count: .counts.operations, event_count: .counts.events, native_ops: [.operations[] | select(.tier=="native") | .id], event_types: [.events[] | .event_type] }'The response shape:
{ "operations": [...], "events": [...], "ai_tools": [...], "counts": { "operations": 14, "events": 22, "native_operations": 11, "plugin_operations": 3, "ai_tools": 9 }}The catalog rebuilds on demand with a 3-second TTL cache. A plugin that starts or stops becomes visible within approximately 3 seconds.
To find operations compatible with a given event source - annotated with match (artifact or data) and allowed (whether the current user can author that step):
curl -s "https://<freesdn>/api/v1/fabric/connections/suggest?source_event=cameras.event.motion" \ -H "Authorization: Bearer $TOKEN" | jq '.targets[] | {id: .id, match: .match, allowed: .allowed}'Permission requirements summary
Section titled “Permission requirements summary”| Action | Required role / permission |
|---|---|
| Read the catalog | Any authenticated user |
| Suggest compatible operations | Any org member |
Invoke a native op with a permission | Caller must hold that permission; API key scope is the ceiling |
| Invoke a permissionless native sink | Unscoped org-admin |
Invoke a non-native op via /invoke | Not possible - returns 501 |
| Author a Connection step (write op) | Unscoped org-admin + must hold the step’s permission |
| Author a Connection step (permissionless sink) | Unscoped org-admin |
Call POST /api/v1/fabric/ingest | API key with event:write scope |
Honesty caveats
Section titled “Honesty caveats”Next steps
Section titled “Next steps”- The Fabric - catalog structure, executor model, built-in sink reference, SSRF rules, and env vars
- Connections - step-by-step guide to authoring event-to-action Connections
- n8n Integration - community node for bidirectional n8n integration
- Plugin System - declare custom operations and events from an SDK plugin