Promotion and version pinning: dev -> staging -> live
A plugin carries a status from this set: draft, dev, promoting_to_staging, staging, promoting_to_live, live, error. plugsync plugin status <plugin_id> and GET /api/plugins/{id} are where you read it.
draft: created, no bundle deployed yet (or nothing pushed since creation).dev:plugsync plugin pushsucceeded and the dev deploy created or updated your per-developer Lambda function, always invoked at$LATEST. See dev-loop.md.
status and function_arn describe the last deploy that landed, not the highest stage the plugin has reached. A push made after a promotion moves status back to dev and points function_arn at the pusher’s per-developer function once its dev deploy lands. live_version and staging_version are kept, and live connectors keep running the promoted build: they resolve to the shared org function at live_version, not to function_arn.
staging/live: reached viaplugsync plugin promote <id> staging|live(orplugin publish, an alias). Each promotion publishes a new immutable numbered version on the shared, per-org Lambda function — the same function for both targets, just a different version number recorded on the plugin (staging_versionorlive_version).
Nothing in POST /api/plugins/{id}/promote requires the plugin to already be in a particular status before promoting: you can call promote ... live directly from dev without ever promoting to staging first. staging is a real, independently invokable Lambda version, not a mandatory gate.
Promoting
plugsync plugin promote <plugin_id> staging
plugsync plugin promote <plugin_id> live # asks for interactive confirmationPOST /api/plugins/{id}/promote {"target": "staging"|"live"} answers 202 Accepted, sets the plugin’s status to promoting_to_staging/promoting_to_live, and does the AWS work asynchronously — usually within a few seconds. That work is:
- Ensure the shared org function exists, push your currently-stored
bundleas its code, and cut a new immutable numbered Lambda version. - Set
function_arnto that function’s ARN, setlive_versionorstaging_versionto the new version number, movestatustoliveorstaging, and stamplast_deployed_at. - Propagate: the embedded
pluginblock is rewritten in every already-published, active config version that references this plugin bylogical_name— so connectors that were already live pick up the new ARN/version without a fresh publish. Connectors where resolution fails (e.g. a live connector referencing a plugin that has never been promoted to live) are skipped and retried on the next promotion.
Poll plugsync plugin status <plugin_id> until the status leaves promoting_to_*.
How resolution actually works today
Resolution has exactly two automatic paths, keyed off the connector’s mode (only ever "live" or "test", set at connector creation and immutable after):
| Connector | Resolves to |
|---|---|
mode == "test" | The per-developer function at $LATEST (requires the connector’s created_by_id; publish fails otherwise). |
anything else (i.e. mode == "live") | The shared org function at the plugin’s live_version (publish fails if the plugin has never been promoted to live). |
There is no automatic “staging connector” resolution path. staging_version is recorded on promotion but nothing reads it when resolving a connector — promoting to staging publishes a real, invokable Lambda version and updates the plugin’s bookkeeping, but no connector is routed to it automatically. If you need to exercise a staged build against a specific connector before promoting it to live, use the explicit override below.
Pinning a connector to an explicit build
An explicit per-connector override in connector.settings.plugin_overrides wins over both paths above, unconditionally, regardless of connector mode. Take the ARN from plugsync plugin status <plugin_id> right after a promotion has landed (status staging or live) and before anyone pushes again: function_arn always reflects the last deploy, so after a push it names a per-developer function, which has no numbered versions.
PATCH /api/connectors/{id} replaces the whole settings object, it does not merge into it. Read the current settings first and send them back with plugin_overrides added, or every other connector setting is lost:
curl -s "$BASE/api/connectors/$CONNECTOR_ID" -H "Authorization: Bearer $JWT" \
| jq '{settings: (.settings + {plugin_overrides: {"my-plugin": {"function_arn": "arn:aws:lambda:eu-central-1:...:function:<the plugin function>", "version": 3}}})}' \
> patch.json
curl -X PATCH "$BASE/api/connectors/$CONNECTOR_ID" -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-d @patch.jsonversion can be a numbered version (int, e.g. a plugin’s staging_version) or the literal string "$LATEST". This is the only mechanism today for pointing a given connector at a specific plugin build outside the test/$LATEST and live/live_version defaults — for example, running a staging build against one connector before promoting it org-wide to live.
Where the resolved coordinates actually end up
You never author function_arn/version/capabilities on a flow step. You author:
{"id": "generate_doc", "action": "plugin",
"plugin": {"logical_name": "acme_doc_generator", "config": {"store_keys": ["$.payload.object.id"]}}}At publish time, plugsync looks the plugin up by (org, logical_name), resolves it as above, and rewrites the step’s plugin block into the full embedded form:
{"logical_name": "acme_doc_generator",
"function_arn": "arn:aws:lambda:eu-central-1:...:function:<the plugin function>",
"version": 3,
"capabilities": {"http": ["generate-doc.example.cloudfunctions.net"], "store": ["read", "write"], "auth": ["doc_generator"]},
"config": {"store_keys": ["$.payload.object.id"]}}capabilities here always comes from the plugin’s current capabilities, never from anything authored on the step. This resolved block — not the authored one — is what the published manifest carries and what runs, so the coordinates never have to be looked up while an event is in flight. Publishing fails with 422 if the referenced plugin doesn’t exist in your org, or if resolution itself fails (e.g. a live connector referencing a plugin with no live_version yet).
When a re-publish is (and isn’t) needed
Promoting a plugin does not, by itself, require anyone to touch draft/publish
at all: the propagation step above already rewrites the embedded
function_arn/version in every already-published, active config version
that references the plugin, in place. A live connector picks up a promote to
live without a fresh publish.
Two cases are NOT covered by that automatic propagation, and are exactly what
a forced re-publish (POST /publish {"force": true}, or
plugsync push --publish --republish) is for:
- A
plugin_overrideschange on a connector’s own settings. Pinning a connector to an explicit build (see above) only takes effect on that connector’s NEXT publish; nothing re-resolves the active version’s embedded coordinates just because you changed the override. - A promote that failed to propagate to one connector (for example, a live connector whose plugin had not been promoted to live yet at the time): it is retried on the next promote of the same plugin, but a re-publish of that one connector applies it sooner.
GET /preview’s resolved_changes list (see
draft-publish.md)
tells you which of these, if any, currently applies to a connector: it
compares the active version’s embedded plugin coordinates against what
publishing would resolve right now, so you know whether a forced re-publish
would actually change anything before running one.