Skip to main content

Plugin Actions

Actions are plugin-contributed operations that appear in the UI alongside Resources, Notes, and Groups, collecting user input through typed parameters and running synchronously or asynchronously against specific entity types or content types.

Registering an Action

Register actions during init() using mah.action(table):

function init()
mah.action({
id = "colorize",
label = "Colorize Image",
entity = "resource",
placement = {"detail", "card"},
filters = {
content_types = {"image/jpeg", "image/png"}
},
params = {
{ name = "style", type = "select", label = "Style", options = {"realistic", "artistic"}, default = "realistic" },
{ name = "intensity", type = "number", label = "Intensity", min = 1, max = 100, default = 50 }
},
async = true,
confirm = "This will process the image. Continue?",
handler = function(ctx)
local resource = mah.db.get_resource(ctx.entity_id)
-- process the resource...
mah.job_progress(ctx.job_id, 50, "Processing...")
-- ...
mah.job_complete(ctx.job_id, { message = "Done" })
end
})
end

Registration Fields

FieldTypeRequiredDefaultDescription
idstringYes--Unique ID within the plugin (normalized to lowercase)
labelstringYes--Display label in the UI
entitystringYes--Target entity: "resource", "note", or "group"
handlerfunctionYes--Lua function called when the action runs
descriptionstringNo""Optional description
iconstringNo""Optional icon identifier
placementtableNo{"detail"}Where to show: "detail", "card", "bulk"
filterstableNomatch allContent-type, category, or note-type filters
paramstableNononeUser input parameter definitions
asyncbooleanNofalseRun asynchronously via the job system
confirmstringNo""Confirmation message shown before execution
bulk_maxnumberNo0Maximum entities for bulk execution (0 = unlimited)

Registering a duplicate id within the same plugin raises a Lua error.

Action Parameters

Parameters define the input fields shown to the user before the action runs.

FieldTypeRequiredDescription
namestringYesParameter key passed to the handler
typestringYes"text", "textarea", "number", "select", "boolean", "hidden", "info", "entity_ref"
labelstringYesDisplay label
requiredbooleanNoWhether the field must be filled
defaultanyNoDefault value
optionstableNoChoices for "select" type
minnumberNoMinimum value for "number" type
maxnumberNoMaximum value for "number" type
stepnumberNoStep increment for "number" type

Entity Reference Parameters

The entity_ref param type lets a plugin action accept references to one or more resources, notes, or groups as additional input. Use cases: an image-edit action that takes multiple source images, a "merge two notes" action, a "tag groups by another group's tags" action.

Schema

{
name = "extra_images",
type = "entity_ref",
label = "Additional Images",
entity = "resource", -- "resource" | "note" | "group"
multi = true, -- false → single ID; true → array of IDs
required = false,
min = 0, -- multi only; omit for no minimum
max = 9, -- multi only; omit or set to 0 for no maximum
default = "trigger", -- "trigger" | "selection" | "both" | ""
filters = { content_types = {"image/jpeg", "image/png"} }, -- optional; inherits action.filters when omitted
show_when = { model = {"flux2", "nanobanana2"} }, -- standard show_when
description = "Reference images sent alongside the source.",
}

Behavior

  • The picker UI opens layered over the action modal. It applies the effective filter (per-param filters if set, else inherits action.filters).
  • The handler receives the IDs as ctx.params.<name> — a Lua number for multi=false, a Lua table of numbers for multi=true. Server-side validation guarantees every ID exists and matches the filter at request time.
  • default controls what the picker is prefilled with:
    • "trigger" (default when omitted) — the entity the action was launched from.
    • "selection" — IDs from the current bulk-selection store.
    • "both" — union of trigger and selection (requires multi=true).
    • "" — empty; user picks every entry.
  • Trigger and selection are silently ignored if param.entity does not match the action's launch entity type. (Example: an action declared entity = "resource" with an entity_ref entity = "group" default = "trigger" will open with an empty picker on resource pages, since the trigger resource ID is not a valid group ID.)

Constraints

  • required = true cannot be combined with show_when (any param type, not just entity_ref). The server validates required fields before show_when stripping; a hidden required field would fail validation as missing. Workaround: leave required false and validate in the handler.
  • default = "both" requires multi = true.
  • entity must be one of "resource", "note", "group". Other values are rejected at plugin load time.

show_when Array Values

show_when accepts arrays as any-of equality:

show_when = { model = {"flux2", "flux2pro", "nanobanana2"} }
-- Visible when formValues.model is any of the listed values.

Scalar values continue to use strict equality (existing behavior, unchanged).

Action Filters

Filters control which entities see the action. Empty filters match everything.

filters = {
content_types = {"image/jpeg", "image/png", "image/webp"}, -- Resource content types
category_ids = {5, 12}, -- Group Category IDs
note_type_ids = {3} -- Note Type IDs
}
FilterEntityDescription
content_typesResourceMatch Resources with these MIME types
category_idsGroupMatch Groups with these Category IDs
note_type_idsNoteMatch Notes with these Note Type IDs

If a filter is set but the entity lacks the filtered field, the action does not match.

Placement

PlacementLocation
detailEntity detail page (single entity)
cardEntity card in list views (single entity)
bulkBulk action bar (multiple selected entities)

Synchronous Execution

Sync actions (the default) run within a single request-response cycle. The handler receives a context table and returns a result table.

Timeout: 5 seconds.

mah.action({
id = "tag-by-type",
label = "Auto-Tag by Type",
entity = "resource",
handler = function(ctx)
local resource = mah.db.get_resource(ctx.entity_id)
-- do something quick...
return { success = true, message = "Tagged" }
end
})

Handler Context (Sync)

FieldTypeDescription
entity_idnumberID of the target entity
paramstableUser-supplied parameter values
settingstablePlugin settings

ActionResult

FieldTypeDescription
successbooleanWhether the action succeeded
messagestringMessage displayed to the user
redirectstringOptional URL to redirect to after completion
datatableOptional additional data

Asynchronous Execution

Async actions (async = true) run in a background goroutine via the job system. The API returns immediately with a job_id.

Timeout: 5 minutes. Max concurrent: 3 async actions across all plugins.

mah.action({
id = "process-video",
label = "Process Video",
entity = "resource",
async = true,
handler = function(ctx)
mah.job_progress(ctx.job_id, 10, "Downloading...")
-- long-running work...
mah.job_progress(ctx.job_id, 50, "Processing...")
-- more work...
mah.job_complete(ctx.job_id, { message = "Video processed" })
end
})

Handler Context (Async)

Same as sync, plus:

FieldTypeDescription
job_idstringJob ID for progress reporting

Job Progress Control

FunctionDescription
mah.job_progress(job_id, percent, message)Report progress (0-100). SSE updates throttled to 200ms.
mah.job_complete(job_id, result_table)Mark job as completed. Sets progress to 100.
mah.job_fail(job_id, error_message)Mark job as failed.

If the handler returns without calling mah.job_complete or mah.job_fail, the return value is parsed as an ActionResult and the job is updated accordingly.

Abort

Call mah.abort(reason) from any handler to abort the action:

handler = function(ctx)
local resource = mah.db.get_resource(ctx.entity_id)
if not resource then
mah.abort("Resource not found")
end
-- ...
end

This returns { success = false, message = reason }.

API Endpoints

List Available Actions

GET /v1/plugin/actions
ParameterTypeDescription
entitystringRequired: "resource", "note", or "group"
content_typestringOptional: filter by content type
category_iduintOptional: filter by Category ID
note_type_iduintOptional: filter by Note Type ID
curl "http://localhost:8181/v1/plugin/actions?entity=resource&content_type=image/jpeg"

Run an Action

POST /v1/jobs/action/run
Content-Type: application/json
{
"plugin": "image-processor",
"action": "colorize",
"entity_ids": [42],
"params": { "style": "realistic", "intensity": 75 }
}
  • Sync actions: Returns 200 OK with ActionResult
  • Async actions: Returns 202 Accepted with { "job_id": "abc123..." }
  • Bulk (multiple entity_ids): Returns { "results": [...] } for sync actions or { "job_ids": [...] } for async actions. Respects bulk_max.

Get Action Job Status

GET /v1/jobs/action/job?id={jobId}
curl "http://localhost:8181/v1/jobs/action/job?id=abc123def456"

Returns the current job state including status, progress, message, and result.

  • Plugin System -- plugin installation, configuration, and lifecycle
  • Plugin Hooks -- hook registration, injections, custom pages, and menus
  • Job System -- unified job listing, SSE events, and cleanup behavior
  • Plugin Lua API -- full Lua API reference for the mah module