Nitejar Docs
Build on NitejarPlugin SDK

Handler Reference

Every field and method on the PluginHandler interface.

The PluginHandler<TConfig> interface is what every plugin implements. Here is a complete reference.

Metadata fields

import type { PluginHandler } from '@nitejar/plugin-sdk'

const handler: PluginHandler<MyConfig> = {
  type: 'my-plugin', // Unique identifier. Lowercase, no spaces.
  displayName: 'My Plugin', // Shown in the admin catalog.
  description: 'Does the thing.', // One-liner for the catalog card.
  icon: 'brand-slack', // Tabler icon name (https://tabler.io/icons).
  category: 'messaging', // 'messaging' | 'code' | 'productivity'
  sensitiveFields: ['apiKey'], // Field keys that get encrypted at rest.
  // ...methods
}
FieldTypeRequiredDescription
typestringyesUnique plugin type identifier
displayNamestringyesHuman-readable name for the admin catalog
descriptionstringyesOne-liner for catalog cards
iconstringyesTabler icon name
category'messaging' | 'code' | 'productivity'yesCatalog grouping
sensitiveFieldsstring[]yesConfig keys encrypted at rest

responseMode

Optional. Controls when the agent's response is delivered.

  • 'streaming' (default) — Posts each intermediate assistant message as the agent works. Good for chat integrations where users expect typing indicators.
  • 'final' — Waits for the agent to finish, then posts a single response. Good for webhooks, email, or anything where partial updates are noise.
responseMode: 'final',

setupConfig

Optional. Tells the app UI how to render a setup form when someone creates a new plugin instance.

setupConfig: {
  fields: [
    {
      key: 'apiKey',
      label: 'API Key',
      type: 'password',
      required: true,
      placeholder: 'sk-...',
      helpText: 'Create one at https://example.com/settings/keys',
    },
    {
      key: 'channel',
      label: 'Default Channel',
      type: 'select',
      options: [
        { label: '#general', value: 'general' },
        { label: '#alerts', value: 'alerts' },
      ],
    },
  ],
  credentialHelpUrl: 'https://example.com/docs/api-keys',
  credentialHelpLabel: 'How to get an API key',
  supportsTestBeforeSave: true,
},

SetupField shape

FieldTypeRequiredDescription
keystringyesConfig object key this field maps to
labelstringyesForm label
type'text' | 'password' | 'select' | 'boolean'yesInput type
requiredbooleannoWhether the field must be filled
placeholderstringnoInput placeholder text
helpTextstringnoHint shown below the input
options{ label: string; value: string }[]noChoices for select type

validateConfig(config)

Called with the parsed JSON config object. Return { valid: true } or { valid: false, errors: ['...'] }.

validateConfig(config: unknown): ConfigValidationResult {
  const c = config as MyConfig
  if (!c.apiKey) {
    return { valid: false, errors: ['apiKey is required'] }
  }
  return { valid: true }
}

parseWebhook(request, pluginInstance)

Called when a webhook hits your plugin's endpoint. You get the raw Request and a PluginInstance.

PluginInstance shape

interface PluginInstance {
  id: string // Instance ID
  type: string // Plugin type (matches handler.type)
  config: string | null // JSON string — you parse it yourself
}

Return value: WebhookParseResult

async parseWebhook(
  request: Request,
  pluginInstance: PluginInstance
): Promise<WebhookParseResult> {
  const body = await request.json()
  const config = pluginInstance.config
    ? JSON.parse(pluginInstance.config) as MyConfig
    : {}

  return {
    shouldProcess: true,
    workItem: {
      session_key: `my-plugin:${body.user_id}`,
      source: 'my-plugin',
      source_ref: `msg-${body.id}`,
      title: body.text.slice(0, 120),
      payload: JSON.stringify(body),
    },
    idempotencyKey: `my-plugin-${body.id}`,
    responseContext: { channelId: body.channel },
  }
}

Return { shouldProcess: false } to silently drop the webhook (e.g., bad signature, irrelevant event type).

NewWorkItemData shape

FieldTypeRequiredDescription
session_keystringyesGroups messages into conversations
sourcestringyesSource identifier (usually matches plugin type)
source_refstringyesUnique per message for deduplication
titlestringyesShort summary of the inbound message
payloadstring | nullnoFull payload stored as-is
statusstringnoInitial status (defaults to pending)

postResponse(pluginInstance, workItemId, content, responseContext?, options?)

Called to deliver the agent's response back to whatever sent the webhook.

async postResponse(
  pluginInstance: PluginInstance,
  workItemId: string,
  content: string,
  responseContext?: unknown,
  options?: { hitLimit?: boolean; idempotencyKey?: string }
): Promise<PostResponseResult> {
  const config = JSON.parse(pluginInstance.config!) as MyConfig
  const ctx = responseContext as { channelId: string }
  await sendToApi(config.apiKey, ctx.channelId, content)
  return { success: true, outcome: 'sent' }
}

PostResponseResult shape

FieldTypeDescription
successbooleanWhether delivery worked
outcome'sent' | 'failed' | 'unknown'More specific status
retryablebooleanHint for retry logic
providerRefstringExternal message ID if available
errorstringError message on failure

testConnection(config) (optional)

Called from the app UI "Test Connection" button. Receives the parsed config.

async testConnection(config: MyConfig): Promise<{ ok: boolean; error?: string }> {
  const res = await fetch('https://api.example.com/me', {
    headers: { Authorization: `Bearer ${config.apiKey}` },
  })
  if (!res.ok) return { ok: false, error: `API returned ${res.status}` }
  return { ok: true }
}

Set setupConfig.supportsTestBeforeSave: true to show the button in the app UI.

acknowledgeReceipt(pluginInstance, responseContext?) (optional)

Called right after a webhook is accepted, before the agent starts working. Use it to react with an emoji, send a "thinking..." indicator, or similar.

async acknowledgeReceipt(
  pluginInstance: PluginInstance,
  responseContext?: unknown
): Promise<void> {
  const config = JSON.parse(pluginInstance.config!) as MyConfig
  const ctx = responseContext as { messageId: string }
  await addReaction(config.apiKey, ctx.messageId, 'eyes')
}