Skip to main content

Plugins API

Manage plugins, execute actions, and monitor jobs through the REST API.

Plugin Management

With -auth enabled, every endpoint in this section is admin-only.

List Plugins

GET /v1/plugins/manage

Returns all discovered plugins with their current state (enabled/disabled), metadata, and settings.

curl http://localhost:8181/v1/plugins/manage
[
{
"name": "image-processor",
"version": "1.0.0",
"description": "Processes images using external APIs",
"enabled": true,
"legacy": false,
"api_version": 1,
"capabilities": ["db:read", "http", "image"],
"capability_labels": {
"db:read": "Read your library (resources, notes, groups, tags)",
"http": "Make outbound network requests",
"image": "Transform images"
},
"network": ["api.example.com"],
"allow_private_hosts": false,
"dependencies": ["shared-utils"],
"min_app_version": "1.2.0",
"settings": [
{ "name": "api_key", "type": "password", "label": "API Key", "required": true }
],
"values": { "api_key": "sk-abc123" }
}
]

capabilities is the effective set (db:write implies db:read), and capability_labels gives the human sentence for each one. legacy is true for a plugin that declares no manifest at all, which keeps the full mah surface. An empty network means any public host, the broadest policy rather than the absence of network access. values holds the saved setting values.

Enable Plugin

POST /v1/plugin/enable
Content-Type: application/x-www-form-urlencoded
ParameterTypeDescription
namestringPlugin name to enable
curl -X POST http://localhost:8181/v1/plugin/enable \
-d "name=image-processor"

Required settings must be saved before enabling. Returns an error if required settings are missing. Enabling also records the plugin's declared capabilities as consent, so a plugin that later widens its manifest will not load until an operator enables it again. See Plugin Permissions.

Disable Plugin

POST /v1/plugin/disable
Content-Type: application/x-www-form-urlencoded
ParameterTypeDescription
namestringPlugin name to disable
curl -X POST http://localhost:8181/v1/plugin/disable \
-d "name=image-processor"

Disabling removes all hooks, injections, pages, menus, and actions. In-flight async actions are awaited before the Lua VM is closed. Disable is refused when another enabled plugin depends on this one; the error names the dependents, which must be disabled first.

Save Plugin Settings

POST /v1/plugin/settings?name={pluginName}
Content-Type: application/json
ParameterLocationTypeDescription
namequery or formstringPlugin name
(body)JSON bodyobjectSetting key-value pairs
curl -X POST "http://localhost:8181/v1/plugin/settings?name=image-processor" \
-H "Content-Type: application/json" \
-d '{
"api_key": "sk-abc123",
"model": "quality",
"max_size": 2048
}'

Settings are validated against the plugin's declared setting definitions. Unknown keys are ignored. Boolean settings must be native JSON booleans (true/false, not strings). Number settings must be native JSON numbers (2048, not "2048"). A validation failure answers 422 Unprocessable Entity with {"errors": [...]}.

The body replaces the whole settings object rather than merging into it, so send every setting on each save: a declared setting omitted from the request is cleared. The request body is limited to 64 KB.

Purge Plugin Data

Delete all key-value store data for a plugin. The plugin must be disabled before purging.

POST /v1/plugin/purge-data
Content-Type: application/x-www-form-urlencoded
ParameterTypeDescription
namestringPlugin name to purge data for
curl -X POST http://localhost:8181/v1/plugin/purge-data \
-d "name=image-processor"

Response:

{
"ok": true,
"name": "image-processor"
}
warning

Purging deletes all KV store entries for the plugin. This action is irreversible. The plugin must be disabled first; attempting to purge an enabled plugin returns an error.

Set Scoped Access

Open one plugin to group-limited accounts, or close it again.

POST /v1/plugin/scopedAccess
Content-Type: application/x-www-form-urlencoded
ParameterTypeDescription
namestringPlugin name
allowedstring1, true, on or yes opens the plugin; anything else, including an absent value, closes it
curl -X POST http://localhost:8181/v1/plugin/scopedAccess \
-d "name=image-processor" \
-d "allowed=true"

Response:

{
"ok": true,
"name": "image-processor",
"allow_scoped_principals": true
}

Group-limited users and guests are refused a plugin's pages, JSON endpoints, block and display rendering, and action runs until this is turned on for that plugin. See Plugin Permissions.

List Schedules

GET /v1/plugin/schedules?name={pluginName}
ParameterTypeDescription
namestringPlugin name
curl "http://localhost:8181/v1/plugin/schedules?name=image-processor"
[
{
"scheduleId": "nightly-rollup",
"pluginName": "image-processor",
"everySeconds": 3600,
"overlap": "skip",
"nextDueAt": "2025-03-01T11:00:00Z",
"runs": 12,
"lastStatus": "completed",
"lastError": "",
"lastRunAt": "2025-03-01T10:00:04Z",
"owned": true,
"registered": true
}
]

lastRunAt is present once the schedule has run. lastStatus is completed or failed. registered is false when the row exists but the plugin no longer declares that id, which is what a disabled plugin, a renamed schedule and a removed mah.schedule call all look like. owned is false when the row carries no creator, at which point the schedule has stopped rather than merely lost its label.

Run a Schedule

POST /v1/plugin/schedule/run
Content-Type: application/x-www-form-urlencoded
ParameterTypeDescription
namestringPlugin name
scheduleIdstringThe id the plugin passed to mah.schedule
curl -X POST http://localhost:8181/v1/plugin/schedule/run \
-d "name=image-processor" \
-d "scheduleId=nightly-rollup"

Response:

{
"ok": true,
"name": "image-processor",
"scheduleId": "nightly-rollup",
"started": true
}

The response is sent once the run has started, not when it has finished; the run then reports itself through the action_* events on the SSE stream. 404 means there is no such row. 409 means the plugin no longer declares that id, the row has no owner, or the claim is already held by a run in progress.

List Scheduled Downloads

GET /v1/plugin/scheduled-downloads?name={pluginName}
ParameterTypeDescription
namestringPlugin name
curl "http://localhost:8181/v1/plugin/scheduled-downloads?name=image-processor"
[
{
"id": 17,
"pluginName": "image-processor",
"url": "https://example.com/archive.zip",
"dueAt": "2025-03-01T12:00:00Z",
"status": "pending",
"jobId": "",
"lastError": "",
"attempts": 0,
"owned": true,
"createdAt": "2025-03-01T10:00:00Z",
"updatedAt": "2025-03-01T10:00:00Z"
}
]

Rows are one-shot deferred host downloads created by mah.download.submit with delay or start_at. status is pending, submitted, failed or cancelled. A submitted row normally carries the queue jobId; the accepted fail-closed exception is a crash between reserving the row and submitting to the queue, which can strand a submitted row without a job id until operational reconciliation. claimedAt appears briefly while a scheduler tick holds the submit claim. owned: false on a pending row means the submitting user was deleted and the row has stopped rather than firing as an administrator. Naming a plugin that has no rows returns an empty array.

Cancel a Scheduled Download

POST /v1/plugin/scheduled-downloads/cancel
Content-Type: application/x-www-form-urlencoded
ParameterTypeDescription
iduintScheduled download row id
curl -X POST http://localhost:8181/v1/plugin/scheduled-downloads/cancel \
-d "id=17"

Response:

{
"ok": true,
"id": 17,
"status": "cancelled"
}

Only pending rows can be cancelled. A row that has already been submitted, failed or cancelled answers 409 Conflict.

Plugin Actions

List Available Actions

GET /v1/plugin/actions
ParameterTypeRequiredDescription
entitystringYes"resource", "note", or "group"
content_typestringNoFilter by Resource content type
category_iduintNoFilter by Group Category ID
note_type_iduintNoFilter by Note Type ID
curl "http://localhost:8181/v1/plugin/actions?entity=resource&content_type=image/jpeg"
[
{
"plugin_name": "image-processor",
"id": "colorize",
"label": "Colorize Image",
"entity": "resource",
"placement": ["detail", "card"],
"async": true,
"params": [
{ "name": "style", "type": "select", "label": "Style", "options": ["realistic", "artistic"] }
]
}
]

Run an Action

POST /v1/jobs/action/run
Content-Type: application/json
{
"plugin": "image-processor",
"action": "colorize",
"entity_ids": [42],
"params": {
"style": "realistic"
}
}

Sync actions return 200 OK:

{
"success": true,
"message": "Image colorized",
"redirect": "/resource?id=42"
}

Async actions return 202 Accepted:

{
"job_id": "a1b2c3d4e5f6g7h8"
}

Bulk execution (multiple entity_ids) returns wrapped results:

  • Sync actions: { "results": [...] } -- one entry per submitted entity, in the order they were submitted. A bulk run is not atomic, so an entity whose handler failed carries success: false and a message in its own slot and the response is still 200; a single-entity run keeps its error status instead.
  • Async actions: { "job_ids": [...] }

The bulk_max limit on the action registration is enforced, as is the deployment-wide -max-action-entities cap (default 1000). Exceeding either is a 400.

Parameter validation, entity_ref resolution and the action's own entity filters each answer 400 with {"errors": [...]} rather than the generic error shape, and a filter mismatch on any one entity vetoes the whole batch. A group-limited caller gets 403 when the plugin has not been opened to scoped accounts, or when any named entity lies outside its subtree. See Plugin Permissions.

Get Action Job Status

GET /v1/jobs/action/job
ParameterTypeDescription
idstringJob ID
curl "http://localhost:8181/v1/jobs/action/job?id=a1b2c3d4e5f6g7h8"
{
"id": "a1b2c3d4e5f6g7h8",
"source": "plugin",
"pluginName": "image-processor",
"actionId": "colorize",
"label": "Colorize Image",
"entityId": 42,
"entityType": "resource",
"status": "running",
"progress": 65,
"message": "Applying color model...",
"createdAt": "2025-03-01T10:30:00Z"
}

Plugin Block Rendering

GET /v1/plugins/{pluginName}/block/render

Renders a plugin-defined block type as an HTML fragment. The block editor's frontend calls this endpoint to display plugin blocks.

ParameterLocationTypeRequiredDescription
pluginNamepathstringYesThe plugin that registered the block type
blockIdqueryintegerYesThe block to render
modequerystringYes"view" or "edit"
curl "http://localhost:8181/v1/plugins/my-plugin/block/render?blockId=42&mode=view"

Returns text/html content. The block's type must start with plugin:<pluginName>:, otherwise a 400 error is returned.

See Custom Block Types for details on how plugin block rendering works.

Plugin Display Rendering

POST /v1/plugins/{pluginName}/display/render

Renders a plugin-defined display type as an HTML fragment. The schema-driven metadata display component calls this endpoint when a schema property has x-display: "plugin:<pluginName>:<type>".

Request body (JSON):

FieldTypeRequiredDescription
typestringYesThe display type name (without the plugin:<name>: prefix)
valueobjectYesThe metadata value to render
schemaobjectNoThe JSON Schema of the property
field_pathstringNoDot-notation path of the field
field_labelstringNoDisplay label of the field
entity_typestringNoEntity the metadata belongs to, passed to the renderer as ctx.entity_type
entity_idintegerNoId of that entity, passed as ctx.entity_id; a value above 2^53-1 is rejected with 400
curl -X POST "http://localhost:8181/v1/plugins/my-plugin/display/render" \
-H "Content-Type: application/json" \
-d '{"type":"color-swatch","value":{"hex":"#4f46e5","name":"Indigo"}}'

Returns text/html content. The plugin must have registered the display type via mah.display_type(), otherwise a 500 error is returned. Render timeout is 5 seconds.

See Plugin Lua API for how to register display types.

Plugin Pages

GET|POST /plugins/{pluginName}/{path}

Plugin-registered pages are served at this path. The response is HTML generated by the plugin's page handler.

Two first segments under /plugins/ are reserved and cannot be a plugin page path: manage is the plugin management page, and public serves a plugin's own static assets while that plugin is enabled. See Static Assets.

curl http://localhost:8181/plugins/image-processor/dashboard

Plugin JSON API Endpoints

GET|POST|PUT|DELETE /v1/plugins/{pluginName}/{path}

Plugin-registered JSON API endpoints. Unlike plugin pages (which return HTML), these return application/json responses.

# GET endpoint
curl http://localhost:8181/v1/plugins/my-plugin/stats

# POST with JSON body
curl -X POST http://localhost:8181/v1/plugins/my-plugin/webhook \
-H "Content-Type: application/json" \
-d '{"event": "test"}'

Success Response:

{
"total_notes": 42,
"query": { "page": "1" }
}

Error Responses:

StatusConditionBody
204Handler set no response body(no content)
400Handler called mah.abort(), unless it had already called ctx.status(), in which case that code is used{"error": "reason"}
404Plugin not found or path not registered{"error": "plugin not found"} or {"error": "endpoint not found"}
405Path exists but method not registered{"error": "method not allowed"}
413Request body exceeds 1 MB{"error": "request body too large"}
500Handler runtime error{"error": "internal plugin error"}
504Handler exceeded timeout{"error": "handler timed out after <duration>"} (for example handler timed out after 5s)

See Plugin Lua API Reference for the mah.api() registration function.

Unified Job Endpoints

The queue endpoint returns every job the download manager runs: downloads, group exports and imports, and admin similarity recomputes. Plugin action jobs are not among them; they appear in the SSE event stream alongside download events.

List Jobs

GET /v1/jobs/queue
curl http://localhost:8181/v1/jobs/queue

Returns the retained jobs currently held in the queue manager, including completed, failed, cancelled, or paused jobs until their retention window expires. Plugin action jobs are not included here; they are available only via the SSE event stream below.

SSE Event Stream

GET /v1/jobs/events

Server-Sent Events stream for all job types. The stream uses SSE event names to distinguish job types.

Download events use event names added, updated, removed:

event: updated
data: {"type":"updated","job":{"id":"abcd1234","status":"downloading","progress":45}}

Plugin action events use event names action_added, action_updated, action_removed:

event: action_updated
data: {"job":{"id":"a1b2c3d4e5f6g7h8","source":"plugin","status":"running","progress":65}}

Initialization: On connect, an init event is sent with all current jobs:

event: init
data: {"jobs":[...],"actionJobs":[...]}