Skip to Content
ReferenceConfig V3Examples

Examples

Three complete configs you can copy, paste into PATCH /draft, and publish. Each one declares the event sources its flows read from in the top-level sources key, so the draft is complete on its own; the only setup outside the JSON is a credential the third example refers to by name (see its “Prerequisites”). All three are validated against the published config schema on every change, and their full JSON is embedded below rather than only linked: the docs-site (docs.plugsync.com) does not serve underscore-prefixed directories, so a file living only under _examples/ is unreachable from the rendered page. Copy the JSON from here.

1. acme-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"} - the upsert matches the HubSpot company by hs_tax_id and creates one when no company has that tax id. Only property drives the match; strategy is a descriptive label. See entities.md “Identity”.
  • field_mappings[] - read from $.payload.* directly (the inbound webhook body), no canonical hop.
  • flows[0] matches (source: "acme_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".
  • sources[0] declares acme_inbound as a webhook source. Its config.inbound says how a delivery is checked and routed: verify_signature: "hmac_sha256" (required: the signature is the webhook’s only authentication) and event_type_path: "$.event", the payload field the event type is read from (default $.event_type). The signing secret is not part of the config: the publish that creates the source returns it once, and POST .../sources/acme_inbound/rotate-secret issues a new one. See event sources.

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/acme-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": "acme_company_upsert", "source": "acme_inbound", "event_type": "upsert_company", "event_type_from": "$.event", "steps": [ {"id": "upsert_hubspot", "action": "upsert", "target": "hubspot", "entity": "company", "data": "$.payload"} ] } ], "sources": [ { "name": "acme_inbound", "type": "webhook", "config": {"inbound": {"verify_signature": "hmac_sha256", "event_type_path": "$.event"}} } ], "settings": {} }

2. fooshop - federated mode

A complete worked example of a federated config, reproduced in full.

Key features:

  • Two entities (contact, order), both classified federated 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 (the fooshop_shop_stage plugin) 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).
  • Both flows read from one webhook source, fooshop_events, declared in sources the same way as in acme-light. It reads the event type from $.event, so the shop sends {"event": "signup", ...} or {"event": "order.paid", ...}; change event_type_path if your payloads carry it elsewhere.

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", "plugin": { "logical_name": "fooshop_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" } ] } ] } ], "sources": [ { "name": "fooshop_events", "type": "webhook", "config": {"inbound": {"verify_signature": "hmac_sha256", "event_type_path": "$.event"}} } ], "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 a pattern that runs in production against an external CRM (orbit-crm in the examples of this reference).

Both event sources this flow uses (the hubspot source with its triggers, the rest source with the outbound base_url and credential) are declared in the config’s top-level sources key: flows[].source and a step’s target are NAME references into that list. See “Event sources” below for what each entry does.

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 “Event sources” 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 names the plugin (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 view of the step context. 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.

Event sources (in this JSON)

The sources key at the bottom of the config declares the two sources the flow and the plugin call use. Publishing the config creates them (or updates them, if they already exist under the same name); the same definitions are what POST /api/connectors/<id>/sources writes into the draft, answered 202 pending_publish until you publish.

hubspot_journal, 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"} ] } }

erp_outbound, a rest-type source providing the ERP’s base_url and the name of an oauth2_client_credentials credential (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" } } }

Prerequisites (outside this JSON)

The credential erp_outbound names must exist before you save the draft, because the draft write checks that every credential_name resolves. Create it first with POST /api/credentials:

{ "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" } } ] } ], "sources": [ { "name": "hubspot_journal", "type": "hubspot", "config": { "triggers": [ {"property_name": "dealstage", "pattern": "^closedwon$", "event_type": "deal.qualified"} ] } }, { "name": "erp_outbound", "type": "rest", "config": { "outbound": { "base_url": "https://erp.example.com/api/v2", "credential_name": "ERP outbound OAuth2" } } } ], "settings": {} }

Validating your own config

Three checks, cheapest first:

  1. Structural, offline. GET /api/config-surface serves the JSON Schema for the whole config document; validate your file against it locally before you send anything. See config-surface.md.
  2. Schema and document-level validators, no side effects. Save the config to the draft (PATCH /api/connectors/{id}/draft). An invalid document is refused with a 422 naming the offending field, and a draft that does save is still never in effect until you publish it. This check is confined to the config document itself: it does not resolve anything outside it.
  3. Cross-resource checks, before you publish. Whether a flow’s source names a real EventSource, whether a step’s target and its credential_name exist, whether the portal matches what the config expects: all of that is checked at publish time, and GET /api/connectors/{id}/preflight reports it without publishing (a HubSpot schema or property mismatch, an echo/conflict overlap, an unreachable event type, and so on). GET /api/connectors/{id}/preview shows the diff the publish would apply.
Last updated on