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 namedhubspot,infer_entity_modereturnssimple. The dashboard renders the single-schema editor.schemas.hubspot.identity = {"strategy": "match_or_create", "property": "hs_tax_id"}- the upsert matches the HubSpot company byhs_tax_idand creates one when no company has that tax id. Onlypropertydrives the match;strategyis 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"), 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".sources[0]declaresacme_inboundas awebhooksource. Itsconfig.inboundsays how a delivery is checked and routed:verify_signature: "hmac_sha256"(required: the signature is the webhook’s only authentication) andevent_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, andPOST .../sources/acme_inbound/rotate-secretissues 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.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": "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 (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 (thefooshop_shop_stageplugin) 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).
-
Both flows read from one
webhooksource,fooshop_events, declared insourcesthe same way as in acme-light. It reads the event type from$.event, so the shop sends{"event": "signup", ...}or{"event": "order.paid", ...}; changeevent_type_pathif 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 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 “Event sources” 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_namenames the plugin (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 own view of the step context. 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.
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:
- Structural, offline.
GET /api/config-surfaceserves the JSON Schema for the whole config document; validate your file against it locally before you send anything. See config-surface.md. - 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. - Cross-resource checks, before you publish. Whether a flow’s
sourcenames a real EventSource, whether a step’stargetand itscredential_nameexist, whether the portal matches what the config expects: all of that is checked at publish time, andGET /api/connectors/{id}/preflightreports it without publishing (a HubSpot schema or property mismatch, an echo/conflict overlap, an unreachable event type, and so on).GET /api/connectors/{id}/previewshows the diff the publish would apply.