Examples
Three complete configs you can copy, paste into PATCH /draft, and publish.
All three are syntactically valid (CI runs ConnectorConfigV3.model_validate
on each on every push) and their full JSON is embedded below, not just
linked — a link to _examples/*.json alone is not enough: the docs-site
mirror (docs.plugsync.com, synced from this repo) does not carry
underscore-prefixed directories, so a file living only under _examples/
is unreachable from the published page. The files still exist in this repo
under _examples/
too, for a plain curl -d @file.json copy without pasting out of a
rendered page.
1. compass-light - simple mode
The smallest possible useful config: one entity (company), one schema
(hubspot), four field mappings, one inbound flow.
Read top to bottom:
version: "3.0"- pinned.entities[0].name = "company"- the entity name; steps reference it.entities[0].schemas.hubspot- the only schema. Because there is exactly one schema and it is namedhubspot,infer_entity_modereturnssimple. The dashboard renders the single-schema editor.schemas.hubspot.identity = {"strategy": "match_or_create", "property": "hs_tax_id"}- HubSpot side does a lookup byhs_tax_id; if no company has that tax id, create one. See entities.md “Identity”.field_mappings[]- read from$.payload.*directly (the inbound webhook body), no canonical hop.flows[0]matches(source: "compass_inbound", event_type: "upsert_company"), withevent_type_from: "$.event"recording the payload path that carries the event type. The single stepupsert_hubspotupserts the whole payload into the company schema viadata: "$.payload".
To deploy this config:
curl -X PATCH https://app.plugsync.com/api/connectors/<id>/draft \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d @docs/content/reference/config-v3/_examples/compass-light.jsonThen publish from the dashboard (“Review changes” -> “Publish v1”).
Full config
{
"version": "3.0",
"entities": [
{
"name": "company",
"schemas": {
"hubspot": {
"object_type": "companies",
"identity": {"strategy": "match_or_create", "property": "hs_tax_id"},
"field_mappings": [
{"source": "$.payload.name", "target": "name"},
{"source": "$.payload.vat_number", "target": "hs_tax_id"},
{"source": "$.payload.city", "target": "city"},
{"source": "$.payload.country", "target": "country"}
]
}
}
}
],
"flows": [
{
"name": "compass_company_upsert",
"source": "compass_inbound",
"event_type": "upsert_company",
"event_type_from": "$.event",
"steps": [
{"id": "upsert_hubspot", "action": "upsert", "target": "hubspot",
"entity": "company", "data": "$.payload"}
]
}
],
"settings": {}
}2. fooshop - federated mode
The real config that (internal plugsync tooling - see the source repo) builds. The setup script remains the canonical source; the JSON here is a snapshot.
Key features:
-
Two entities (
contact,order), both classified federated byinfer_entity_modebecause each has two schemas (fooshopandhubspot). -
The flows transform from the source schema into a canonical shape via the
$.canonical.*JSONPath, then upsert from canonical to HubSpot:{"id": "to_canonical", "action": "transform", "entity": "contact", "from_schema": "fooshop", "to_schema": "canonical"}followed by an
upsertagainsttarget: "hubspot"that reads$.canonical. Thefooshop_signupflow has exactly this pair (onetransform+ oneupsert). Thefooshop_order_paidflow extends the pattern: twotransformsteps (one forcontact, one fororder), apluginstep (compute_shop_stagefromplugins.fooshop.shop_stage) that enriches the canonical state in between, and twoupsertsteps (contact then deal, with the deal carrying anassociationsentry{"to_entity": "contact", "to_external_id": "$.canonical.email"}- that links it back to the upserted contact, see step-actions.md).
This hub-and-spoke pattern is the standard shape for any case where multiple sources need to converge on a single target representation (or vice versa). See simple-vs-federated.md for when to reach for it, and step-actions.md for the per-action reference.
Full config
{
"version": "3.0",
"entities": [
{
"name": "contact",
"schemas": {
"fooshop": {
"identity": {
"strategy": "external_id",
"field": "id"
},
"field_mappings": [
{
"source": "$.email",
"target": "$.canonical.email"
},
{
"source": "$.firstName",
"target": "$.canonical.firstname"
},
{
"source": "$.lastName",
"target": "$.canonical.lastname"
},
{
"source": "$.shopDomain",
"target": "$.canonical.company_domain"
}
]
},
"hubspot": {
"object_type": "contacts",
"identity": {
"strategy": "by_email",
"property": "email"
},
"field_mappings": [
{
"source": "$.canonical.email",
"target": "email"
},
{
"source": "$.canonical.firstname",
"target": "firstname"
},
{
"source": "$.canonical.lastname",
"target": "lastname"
},
{
"source": "$.canonical.company_domain",
"target": "fooshop_shop_domain"
}
]
}
}
},
{
"name": "order",
"schemas": {
"fooshop": {
"identity": {
"strategy": "external_id",
"field": "id"
},
"field_mappings": [
{
"source": "$.id",
"target": "$.canonical.id"
},
{
"source": "$.totalAmount",
"target": "$.canonical.amount"
},
{
"source": "$.customerEmail",
"target": "$.canonical.customer_email"
},
{
"source": "$.status",
"target": "$.canonical.status"
}
]
},
"hubspot": {
"object_type": "deals",
"identity": {
"strategy": "external_id",
"property": "fooshop_order_id"
},
"field_mappings": [
{
"source": "$.canonical.id",
"target": "fooshop_order_id"
},
{
"source": "$.canonical.amount",
"target": "amount"
}
]
}
}
}
],
"flows": [
{
"name": "fooshop_signup",
"source": "fooshop_events",
"event_type": "signup",
"steps": [
{
"id": "to_canonical",
"action": "transform",
"entity": "contact",
"from_schema": "fooshop",
"to_schema": "canonical"
},
{
"id": "upsert_contact",
"action": "upsert",
"target": "hubspot",
"entity": "contact",
"data": "$.canonical"
}
]
},
{
"name": "fooshop_order_paid",
"source": "fooshop_events",
"event_type": "order.paid",
"steps": [
{
"id": "contact_to_canonical",
"action": "transform",
"entity": "contact",
"from_schema": "fooshop",
"to_schema": "canonical"
},
{
"id": "order_to_canonical",
"action": "transform",
"entity": "order",
"from_schema": "fooshop",
"to_schema": "canonical"
},
{
"id": "compute_stage",
"action": "plugin",
"module": "plugins.fooshop.shop_stage",
"function": "compute_shop_stage"
},
{
"id": "upsert_contact",
"action": "upsert",
"target": "hubspot",
"entity": "contact",
"data": "$.canonical"
},
{
"id": "upsert_deal",
"action": "upsert",
"target": "hubspot",
"entity": "order",
"data": "$.canonical",
"associations": [
{
"to_entity": "contact",
"to_external_id": "$.canonical.email"
}
]
}
]
}
],
"settings": {}
}3. hubspot-triggers-to-rest - outbound, plugin write, write-back
The flagship outbound shape: HubSpot is the source of truth, a property
change on a HubSpot record drives a write to an external REST system, and the
external system’s own id is written back onto the HubSpot record so later
events can be matched by external_id instead of re-searching. This is a
trimmed, generic version of the pattern the PNO/MatchPoint connector runs in
production (backend/scripts/scenarios/pno/runs/2026-07-19-run2/connector-config.json’s
trigger_create_mp flow).
Two pieces of setup live OUTSIDE this JSON, on purpose: event sources
(triggers, outbound base_url/credential) are a connector-level concern
configured via POST /api/connectors/<id>/sources, never part of
ConnectorConfigV3 — flows[].source and a step’s target are just NAME
references into that separate registry. See “Prerequisites” below for the
two sources this flow assumes exist.
Read top to bottom:
entities[0].name = "deal"has two schemas -> federated byinfer_entity_mode(any entity with more than one schema is federated, regardless of the schema names — see simple-vs-federated.md):schemas.hubspot-object_type: "deals",identity: {"strategy": "external_id", "property": "erp_deal_id"}. Nofield_mappings: this flow never upserts INTO HubSpot (HubSpot is the source here), but the identity block still declares the propertywrite_back_external_id(below) fills in, so a later inbound flow on the same connector could resolve this deal byerp_deal_id.schemas.hubspot_inbound- a lookup-only schema whose single field_mapping resolves the deal’s HubSpot owner id into an email via theresolve: "owner_id_to_email"marker (see entities.md “Owner resolution”):{"source": "$.payload.object.properties.hubspot_owner_id", "target": "$.canonical.owner_email", "resolve": "owner_id_to_email"}.
flows[0].source = "hubspot_journal"- the name of ahubspot-type event source (see “Prerequisites” below), not a literal type. Itsconfig.triggersdecides which property changes actually reach this flow.flows[0].event_type = "deal.qualified"- NOT one of the default{objectTypeId}.{create|update|delete}journal events (step-actions.md’shubspot_event_type_conventionlimit): atriggersentry fires a custom event_type of your choosing on a property match, so a config author can route exactly the property change that matters (here:dealstagereachingclosedwon) to its own dedicated flow instead of gating every plain.updateevent with awhencondition.steps[0](map_owner, actiontransform) projectsschemas.hubspot_inboundinto canonical, so$.canonical.owner_emailis available to every later step — the declarative half of “resolve the owner id the trigger payload carries into something the plugin/REST call actually needs downstream” (an email, not a HubSpot-internal owner id).steps[1](push_erp, actionplugin) is the imperative escape hatch, not the declarativepostaction: writing to this particular ERP needs branching/stateful logic a manifest step can’t express (this is the reason plugins exist at all — see step-actions.md’splugin).plugin.logical_nameis the Lambda form (resolved to afunction_arn+versionat publish time, never authored by hand);plugin.configis a static, per-step JSON blob the plugin reads at invocation time, here forwarding the$...ref the plugin resolves against its ownStepExecutionContextview. The plugin performs the actual outbound HTTP call against therest-type event source (OAuth2 client-credentials auth resolved and cached by the runtime, not by plugin code) and returns{"id": "<erp record id>", ...}, written toctx.outputs["push_erp"]and so readable by later steps as$push_erp.id.steps[2](write_back_external_id, actionupdate_by_id) is the write-back: PATCHes the HubSpot deal (object_id: "$.payload.object.id", the id the triggering journal event already carries — no search, no re-lookup) witherp_deal_idset to the id the plugin just returned. This is what makes the identity block onschemas.hubspotmeaningful: once this runs, a bidirectional connector’s inbound side can resolve the same deal byerp_deal_idinstead of re-matching some other property.
Prerequisites (outside this JSON)
Two event sources this flow’s source/plugin call assume already exist,
created via POST /api/connectors/<id>/sources — neither is part of
ConnectorConfigV3, both are referenced from it purely by name:
A hubspot-type source whose triggers decide which property change
becomes deal.qualified:
{
"name": "hubspot_journal",
"type": "hubspot",
"config": {
"triggers": [
{"property_name": "dealstage", "pattern": "^closedwon$", "event_type": "deal.qualified"}
]
}
}A rest-type source providing the ERP’s base_url and an
oauth2_client_credentials credential (created separately via
POST /api/credentials, then referenced here by name — the client
secret is never inlined into the source’s own config):
{
"name": "erp_outbound",
"type": "rest",
"config": {
"outbound": {
"base_url": "https://erp.example.com/api/v2",
"credential_name": "ERP outbound OAuth2"
}
}
}{
"name": "ERP outbound OAuth2",
"credential_type": "oauth2_client_credentials",
"data": {
"token_url": "https://erp.example.com/oauth/token",
"client_id": "plugsync-connector",
"client_secret": "<secret>",
"scope": "deals.write"
}
}The credential fetches and caches a Bearer token via the client-credentials grant (RFC 6749 §4.4) automatically — no per-call token handling in the plugin.
Full config
{
"version": "3.0",
"entities": [
{
"name": "deal",
"schemas": {
"hubspot": {
"object_type": "deals",
"identity": {"strategy": "external_id", "property": "erp_deal_id"}
},
"hubspot_inbound": {
"field_mappings": [
{
"source": "$.payload.object.properties.hubspot_owner_id",
"target": "$.canonical.owner_email",
"resolve": "owner_id_to_email"
}
]
}
}
}
],
"flows": [
{
"name": "deal_qualified_to_erp",
"source": "hubspot_journal",
"event_type": "deal.qualified",
"steps": [
{
"id": "map_owner",
"action": "transform",
"entity": "deal",
"from_schema": "hubspot_inbound",
"to_schema": "canonical"
},
{
"id": "push_erp",
"action": "plugin",
"plugin": {
"logical_name": "erp_deal_sync",
"config": {
"owner_email_ref": "$.canonical.owner_email"
}
}
},
{
"id": "write_back_external_id",
"action": "update_by_id",
"target": "hubspot",
"entity": "deal",
"object_id": "$.payload.object.id",
"data": {
"erp_deal_id": "$push_erp.id"
}
}
]
}
],
"settings": {}
}Validating your own config
The validator that runs in CI is callable standalone. Drop your JSON next to the existing examples and run:
(internal plugsync tooling - see the source repo)
(You’ll also need to add an entry to EXPECTED_MODES in validate.py for
the new file.)