Skip to Content
ReferencePluginsLifecycle hooks

Lifecycle hooks: setup and teardown

A plugin is invoked for two reasons. The usual one is an event reaching a plugin step. The other is a lifecycle hook: one call when the plugin starts serving a connector (setup) and one when it stops (teardown). Use them for work that belongs to the pair plugin + connector and not to a single event, for example registering a webhook on the system you integrate with and removing it again.

Hooks are optional. A plugin that ignores them works, with one condition: its handler must tell a hook apart from an event. Both reach the same handler export, and the hook payload has none of the fields an event carries. See Recognizing a hook.

The hook payload

A hook is not a PluginPayload. Your handler receives exactly this:

{ "schema_version": 1, "hook_type": "setup", "connector_id": "5d0c0f0e-7c1b-4c8e-9a53-0d6f5a1b2c3d", "plugin_name": "acme_doc_generator", "cycle": 1 }
FieldTypeNotes
schema_versionnumber1.
hook_type"setup" or "teardown"Which hook this is. Never present on an event.
connector_idstring (UUID)The connector the plugin is being set up or torn down for.
plugin_namestringThe plugin’s logical name, the one a plugin step references.
cyclenumberStarts at 1 for the first setup of this plugin on this connector. A setup that follows a successful teardown opens the next cycle. The teardown of a cycle carries the same number as its setup.

What is not there: payload, config, outputs, store, auth, and every other field described in payload-contract.md. A hook cannot read the step’s config, and it gets no store snapshot and no resolved auth headers. Code that reads event.payload.something without checking first throws on a hook.

Recognizing a hook

Branch on hook_type before anything else, and return early:

interface LifecycleHookPayload { schema_version: number; hook_type: "setup" | "teardown"; connector_id: string; plugin_name: string; cycle: number; } export async function handler(event: PluginPayload | LifecycleHookPayload) { if ("hook_type" in event) { if (event.hook_type === "setup") { // one-time work for event.connector_id } // nothing to do on teardown return { ok: true }; } // from here on, event is a PluginPayload const order = event.payload; // ... }

A plugin with nothing to set up still needs the first two lines of that branch (if ("hook_type" in event) return {};), so that a hook is answered instead of running the event code against a payload that is not an event.

What the return value and an exception do

Returning normally is a success, whatever you return and whether or not your code did anything for that hook_type. A JSON object you return is kept as the hook’s response. The fields of a PluginResponse (action, outputs, store_writes, outcome) have no effect on a hook: there is no flow to act on.

An uncaught exception is a failure of the hook, and only of the hook:

  • The hook is recorded as failed and retried automatically, a limited number of times with a growing delay. A handler that can run twice must therefore be safe to run twice (check before you register that webhook).
  • While it is failing, plugsync plugin status <plugin_id> and GET /api/plugins/{id} show an error_message that names the hook: Setup hook failed: the plugin's own handler raised an error. Check 'plugsync plugin logs' for the handler output. The message never carries your stack trace; plugsync plugin logs does.
  • If every retry fails, the plugin’s status becomes error.
  • The promotion is not rolled back. The version you promoted is already serving the connector when the setup hook runs, and it keeps receiving events while the hook is failing and after it has failed for good. If your event code depends on what setup prepares, make it check, do not assume the hook has succeeded.
  • The next hook that succeeds for the plugin clears the message and puts the plugin back in the status its versions describe (dev, staging, live).

To recover: fix the handler, plugsync plugin push, promote if the connector is live, and request the hook again (below). A new request replaces an earlier attempt of the same hook that is still being retried.

When a hook runs

HookRuns
setupAutomatically, after a promotion has rolled the plugin out to a connector whose published config uses it, if no setup is already in effect for that connector. Or when you request it.
teardownAutomatically when the connector is deleted, for every plugin its published config uses. This is best effort: it never blocks or fails the delete. Or when you request it.

The hook runs on the same function version that serves the connector’s events: the promoted version on a live connector, your latest push on a test connector.

Requesting a hook

There is no CLI command for hooks. Request them through the API, with the same authentication as every other plugin endpoint:

curl -X POST https://app.plugsync.com/api/plugins/{plugin_id}/lifecycle/setup \ -H "Authorization: Bearer $TOKEN" \ -H "content-type: application/json" \ -d '{"connector_id": "5d0c0f0e-7c1b-4c8e-9a53-0d6f5a1b2c3d"}'

POST /api/plugins/{plugin_id}/lifecycle/teardown takes the same body. Both answer 202 Accepted (the hook is queued, it has not run yet) with the hook’s record:

{ "id": "0b6c...", "plugin_id": "c1d2...", "connector_id": "5d0c0f0e-7c1b-4c8e-9a53-0d6f5a1b2c3d", "hook_type": "setup", "cycle": 1, "status": "pending", "invoked_at": "2026-03-01T10:15:00Z", "completed_at": null, "response": null, "error": null }

status is pending, success or failed. Requests are idempotent within a cycle: asking for a setup that is already pending, or that already succeeded in the current cycle, queues nothing and returns the existing record, so repeating the same POST is also how you read the outcome (status, response, error). A setup that failed is queued again. The same holds for teardown.

StatusMeaning
202Queued, or already in effect for the current cycle (see status in the body).
404No such plugin or connector in your org. For teardown, also: no setup was ever recorded for this plugin and connector, so there is nothing to tear down.
409The plugin has no function to run for this connector yet, typically a live connector and a plugin that was never promoted to live. The detail says which.

The plugin itself also carries setup_completed_at in GET /api/plugins/{id}: the time of its last successful setup on any connector, not on a particular one. For the state on one connector, repeat the POST above and read status.

Last updated on