Skip to Content
ReferenceConfig V3Examples

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 named hubspot, infer_entity_mode returns simple. The dashboard renders the single-schema editor.
  • schemas.hubspot.identity = {"strategy": "match_or_create", "property": "hs_tax_id"} - HubSpot side does a lookup by hs_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"), with event_type_from: "$.event" recording the payload path that carries the event type. The single step upsert_hubspot upserts the whole payload into the company schema via data: "$.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.json

Then 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 by infer_entity_mode because each has two schemas (fooshop and hubspot).

  • 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 upsert against target: "hubspot" that reads $.canonical. The fooshop_signup flow has exactly this pair (one transform + one upsert). The fooshop_order_paid flow extends the pattern: two transform steps (one for contact, one for order), a plugin step (compute_shop_stage from plugins.fooshop.shop_stage) that enriches the canonical state in between, and two upsert steps (contact then deal, with the deal carrying an associations entry

    • {"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 ConnectorConfigV3flows[].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 by infer_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"}. No field_mappings: this flow never upserts INTO HubSpot (HubSpot is the source here), but the identity block still declares the property write_back_external_id (below) fills in, so a later inbound flow on the same connector could resolve this deal by erp_deal_id.
    • schemas.hubspot_inbound - a lookup-only schema whose single field_mapping resolves the deal’s HubSpot owner id into an email via the resolve: "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 a hubspot-type event source (see “Prerequisites” below), not a literal type. Its config.triggers decides 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’s hubspot_event_type_convention limit): a triggers entry 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: dealstage reaching closedwon) to its own dedicated flow instead of gating every plain .update event with a when condition.
  • steps[0] (map_owner, action transform) projects schemas.hubspot_inbound into canonical, so $.canonical.owner_email is 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, action plugin) is the imperative escape hatch, not the declarative post action: 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’s plugin). plugin.logical_name is the Lambda form (resolved to a function_arn+version at publish time, never authored by hand); plugin.config is a static, per-step JSON blob the plugin reads at invocation time, here forwarding the $... ref the plugin resolves against its own StepExecutionContext view. The plugin performs the actual outbound HTTP call against the rest-type event source (OAuth2 client-credentials auth resolved and cached by the runtime, not by plugin code) and returns {"id": "<erp record id>", ...}, written to ctx.outputs["push_erp"] and so readable by later steps as $push_erp.id.
  • steps[2] (write_back_external_id, action update_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) with erp_deal_id set to the id the plugin just returned. This is what makes the identity block on schemas.hubspot meaningful: once this runs, a bidirectional connector’s inbound side can resolve the same deal by erp_deal_id instead 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.)

Last updated on