# Job & Plugin: Registration

<figure><img src="/files/p9JQI6igJJauFa2P5pSM" alt=""><figcaption></figcaption></figure>

**Audience:** IT admins who need to know how plugins and jobs are discovered, registered, and loaded by the KEPM agent.

## Plugin Registration

### How Plugins Are Registered

Plugins are discovered from JSON files in the `Plugins/` directory. The agent reads these files at startup, validates them, and launches plugins according to their `startupPriority` and `autoStart` settings. Plugins with `pluginType: Executable` run as separate processes; those with `pluginType: Service` run inside the main agent process.

* **Location:** `{approot}/Plugins/` — for example, `C:\Program Files\KeeperPrivilegeManager\Plugins`
* **File name:** `{PluginName}.json` — for example, `KeeperPolicy.json`

To add a new plugin, place the plugin's binaries under `Plugins/bin/{PluginName}/` and add the plugin JSON file at `Plugins/{PluginName}.json`. After a service restart (or plugin reload if supported), the new plugin is registered and available to be started.

### Plugin JSON Fields

The following fields appear in plugin JSON files. Your plugin may not require all of them — validate against the [Custom Plugin Integration Guide](https://claude.ai/integration/custom-plugin-guide) and your administrator documentation for your agent version.

<table data-header-hidden="false" data-header-sticky><thead><tr><th width="197.3333740234375">Field</th><th>Description</th></tr></thead><tbody><tr><td><code>id</code></td><td>Unique plugin identifier. Must match the filename and the key used in <code>/api/PluginSettings/{id}</code>.</td></tr><tr><td><code>pluginType</code></td><td><code>Executable</code> for a separate process; <code>Service</code> for an in-process component.</td></tr><tr><td><code>executablePath</code></td><td>Path to the plugin binary. Can be relative to the agent root or absolute. Omit for in-process plugins.</td></tr><tr><td><code>supportedPlatforms</code></td><td>Array of OS names. The agent skips the plugin on unsupported platforms.</td></tr><tr><td><code>Subscription</code></td><td>Primary MQTT subscription: <code>Topic</code>, <code>Qos</code>, and <code>CleanSession</code>.</td></tr><tr><td><code>metadata.mqttTopics</code></td><td>Additional <code>subscribe</code> and <code>publish</code> topic permissions beyond the primary subscription.</td></tr><tr><td><code>startupPriority</code></td><td>Controls startup order. Lower numbers start earlier.</td></tr><tr><td><code>autoStart</code></td><td>When <code>true</code>, the agent launches the plugin at startup.</td></tr><tr><td><code>executionContext</code></td><td><code>Service</code>, <code>User</code>, or <code>Interactive</code> — where the plugin process runs.</td></tr><tr><td><code>requiresMonitoring</code></td><td>When <code>true</code>, the agent watches the process and detects unexpected exits.</td></tr><tr><td><code>autoRestart</code></td><td>When <code>true</code>, the agent restarts the plugin if monitoring detects it has stopped.</td></tr></tbody></table>

Example plugin JSON:

```json
{
  "id": "KeeperPolicy",
  "name": "Keeper Policy Service",
  "pluginType": "Executable",
  "executablePath": "{pluginroot}/KeeperPolicy/bin/Release/net8.0/KeeperPolicy",
  "supportedPlatforms": ["Windows", "Linux", "macOS"],
  "Subscription": {
    "Topic": "KeeperPolicy",
    "Qos": 2,
    "CleanSession": true
  },
  "startupPriority": 20,
  "autoStart": true,
  "executionContext": "Service"
}
```

### Plugin Lifecycle

* **Start** — The agent launches the plugin executable or loads the in-process component. The plugin subscribes to MQTT and begins its work.
* **Stop / Restart** — Admins can stop or restart a plugin via the local management API — for example, `POST /api/plugins/{name}/stop` or `/restart`. When `autoRestart: true` is set, the agent restarts the plugin automatically if it exits unexpectedly.
* **Settings** — Plugin settings can be stored in unified storage or in the plugin JSON. Updates delivered by policy or API may require a plugin restart to take effect.

### MQTT Subscription and Publish Permissions

Plugins declare their MQTT access in the plugin JSON. The agent loads these declarations at startup and uses them to enforce which topics the plugin may subscribe to and publish to.

**Primary subscription** — Every plugin must declare one primary MQTT topic in the `Subscription` block. This is the plugin's main inbound topic for requests and control messages.

```json
"Subscription": {
  "Topic": "KeeperPolicy",
  "Qos": 2,
  "CleanSession": true
}
```

**Additional topic permissions** — Plugins can declare extra subscribe and publish permissions under `metadata.mqttTopics`:

```json
"metadata": {
  "mqttTopics": {
    "subscribe": ["AuditMessage", "EventMessages"],
    "publish": ["KeeperLogger", "EventMessages"]
  }
}
```

The agent combines the primary subscription topic with any additional subscribe topics to form the plugin's effective subscription set. Topic validation is enforced at the broker — a plugin can only subscribe or publish to topics permitted by its registration. Both `subscribe` and `publish` arrays support MQTT wildcards (`+` and `#`) where appropriate.

## Job Registration

### How Jobs Are Registered

Jobs are discovered from JSON files in the `Jobs/` directory. The job runner scans this directory at startup and when files change, parses each JSON file, validates it, and registers the job. Valid jobs are then available for scheduling and event triggers. Invalid files are skipped and an error is logged.

* **Location:** `{approot}/Jobs/` — for example, `C:\Program Files\KeeperPrivilegeManager\Jobs`
* **File name:** `{job-id}.json` — the `id` field inside the file must match the filename without the `.json` extension

### What Registration Means for Jobs

A **registered** job is loaded, has a valid structure, and is known to the job runner. It will execute when its trigger fires, provided `enabled` is `true` and any declared `condition` is met.

An **unregistered** job is one whose file is missing, unparseable, or not yet loaded. It will not run regardless of any trigger.

You do not need to register jobs in a separate step. Placing a valid `{id}.json` file in the `Jobs/` directory is sufficient — the job runner picks it up at the next rescan or restart. Jobs can also be created and updated via the HTTP API (`POST /api/Jobs`, `PUT /api/Jobs/{id}`), which writes the JSON and updates the Last Known Good store in a single operation. When Last Known Good is enabled, prefer the API or a `JobUpdate` policy over dropping files directly — hand-edited files may be reverted. See the [Custom Job Integration Guide](https://claude.ai/integration/custom-job-guide#part-3-author-the-job-json) for details.

### Job Loading Order and Precedence

Jobs are loaded in no guaranteed order. Dependencies between jobs should be expressed via `events` and `alternateJobId`, not assumed from load sequence.

If the same job `id` appears in more than one source (for example, both a file and an API write), the agent's last-write-wins behavior determines which definition is active. Prefer a single source of truth — either files managed by policy, or the API — to avoid ambiguity.

#### MQTT Subscription and Publish Permissions

Job tasks that need to communicate over MQTT declare their topic permissions in the `mqttTopics` block on the job root object — not inside the individual task definition.

```json
"mqttTopics": {
  "allowedPublications": ["KeeperLogger"],
  "allowedSubscriptions": []
}
```

When `mqttTopics` is present with at least one entry, the agent injects `KEEPER_JOB_ID` and `KEEPER_JOB_NAME` as environment variables before starting the task process. These are required to form the correct MQTT client ID for the connection.

The broker enforces `allowedPublications` and `allowedSubscriptions` at runtime. A publish or subscribe to a topic not in the declared lists will be denied, even if the MQTT connection itself succeeded. See [Job & Plugin: MQTT Topic Permissions](https://claude.ai/chat/mqtt-topic-permissions) for the full enforcement model and a comparison with plugin topic declarations.

## Summary

<table data-header-hidden="false" data-header-sticky><thead><tr><th width="223.814697265625"></th><th width="246.4444580078125">Plugins</th><th>Jobs</th></tr></thead><tbody><tr><td>Configuration file</td><td><code>Plugins/{PluginName}.json</code></td><td><code>Jobs/{job-id}.json</code></td></tr><tr><td>Discovery</td><td>Scanned at startup from <code>Plugins/</code></td><td>Scanned at startup from <code>Jobs/</code> and on file change</td></tr><tr><td>MQTT permissions</td><td><code>Subscription</code> + <code>metadata.mqttTopics</code></td><td><code>mqttTopics</code> on the job root</td></tr><tr><td>Lifecycle management</td><td><code>autoStart</code>, <code>requiresMonitoring</code>, <code>autoRestart</code></td><td>Triggered by <code>schedule</code>, <code>events</code>, or API</td></tr><tr><td>Adding a new component</td><td>Place JSON + binary, restart the agent</td><td>Place JSON in <code>Jobs/</code> or use <code>POST /api/Jobs</code></td></tr></tbody></table>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.keeper.io/keeperpam/endpoint-privilege-manager/reference/plugin-and-job-registration.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
