Skip to content

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.


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.

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)

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.

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.


Six modules expose native operations in the catalog today. The table below is authoritative - any additions show up in GET /api/v1/fabric/catalog.

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.

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.


You can call a native operation once over HTTP without authoring a Connection:

POST /api/v1/fabric/operations/network.client.list/invoke
Authorization: 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

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).

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

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.

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"}
]
}
}

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"}
]
}
}
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.


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.

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.

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
Event type Notes
gateway.sync.completed Gateway orchestration sync completed across controllers
gateway.brain.offline The gateway brain controller went unreachable
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

The catalog endpoint returns everything in one call:

Terminal window
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):

Terminal window
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}'

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


  • 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

All product names, logos, and brands are property of their respective owners. FreeSDN is an independent project and is not affiliated with or endorsed by the vendors it integrates with. See Trademarks.