Skip to Content

Authoring a flow

Two end-to-end walkthroughs - one inbound, one outbound - then a decision table for picking the right action.

A. Inbound walkthrough - webhook to HubSpot company

Scenario: a shop sends a POST to a webhook whenever a company record changes. Plugsync should upsert that company in HubSpot.

Full config, sources included: _examples/inbound-webhook-to-hubspot.json.

Step 1 - Declare the EventSource

An EventSource’s definition is part of the config document, under its sources key, and the flow’s source field references it by name. Declare shop_webhook there, next to the entity and the flow you are about to write:

{ "name": "shop_webhook", "type": "webhook", "config": { "inbound": { "url_template": "/api/webhooks/{connector_id}/shop_webhook", "verify_signature": "hmac_sha256", "signature_header": "x-shop-signature", "event_type_path": "$.event" } } }

Step 4 sends it to the draft along with everything else, so the source and the flow that reads it go live on the same publish - there is no order to get right. POST /api/connectors/{connector_id}/sources with the same body works too and lands in the same place, answering 202.

No secret here: it is not part of a definition and a secret in that POST body is refused with 422 secret_not_declarable. A webhook source’s secret is generated by the publish that creates the source and handed over once, in that publish’s created_sources; afterwards POST .../sources/shop_webhook/rotate-secret issues a new one on the spot. See event-sources.md for the full API and field reference.

Step 2 - Define the entity

The company entity has a single hubspot schema. Because there is exactly one schema and it is named hubspot, plugsync classifies this entity as simple mode - no canonical schema, no transform step needed. See ../config-v3/simple-vs-federated.md.

{ "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"} ] } } }

identity.property: "hs_tax_id" is what drives the match: on a record plugsync has not seen before, the upsert searches HubSpot by tax ID and creates a company only if none matches. strategy: "match_or_create" is a label for that intent and does not change the behavior (see entities.md). The four field_mappings read directly from $.payload.* - the inbound webhook body.

Step 3 - Write the flow

{ "name": "company_upsert", "source": "shop_webhook", "event_type": "upsert_company", "event_type_from": "$.event", "steps": [ { "id": "upsert_hubspot", "action": "upsert", "target": "hubspot", "entity": "company", "data": "$.payload" } ] }

Two points to note:

  • event_type_from: "$.event" records that the event type string lives at $.event in the webhook body. This is a forward-compat hint only - it is persisted on the flow definition but is not resolved by the runtime. The webhook receiver is responsible for extracting the event type and placing it on the message envelope as event_type before flows are matched. See ../config-v3/flows.md.
  • data: "$.payload" passes the entire inbound payload to the upsert step. The field_mappings declared on the hubspot schema then extract the individual properties.

If the source emits a whole family of related event types (cliente.creato, cliente.aggiornato, …), one flow can cover all of them with a suffix wildcard instead of a copy per event type: "event_type": "cliente.*". See ../config-v3/flows.md#wildcard-event_type for the supported syntax and the most-specific-match-wins precedence rule.

Step 4 - Deploy and publish

Save the full config (entities + flows + sources + settings) to the draft, then publish from the dashboard. The body is sent flat - the config object directly, no {"config": ...} wrapper. PATCH /draft’s legacy wrapper key is patch, not config (see ../config-v3/draft-publish.md#legacy-wrapper-deprecated); sending {"config": {...}} here does not fall back to the deprecated wrapper - it lands as a flat merge patch whose one key happens to be config, which fails with a 422 (unknown top-level config key(s) ['config']) once the merge is validated.

curl -X PATCH https://app.plugsync.com/api/connectors/<connector_id>/draft \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "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": "company_upsert", "source": "shop_webhook", "event_type": "upsert_company", "event_type_from": "$.event", "steps": [ {"id": "upsert_hubspot", "action": "upsert", "target": "hubspot", "entity": "company", "data": "$.payload"} ] } ], "sources": [ { "name": "shop_webhook", "type": "webhook", "config": { "inbound": { "url_template": "/api/webhooks/{connector_id}/shop_webhook", "verify_signature": "hmac_sha256", "signature_header": "x-shop-signature", "event_type_path": "$.event" } } } ], "settings": {} }'

Then open the dashboard, click “Review changes”, and publish as v1. That publish creates the shop_webhook source and returns its signing secret once, in created_sources - copy it, it is not readable again. The running connector usually picks the new version up within a few seconds; GET /api/connectors/{id}/versions/active confirms which version is live.


B. Outbound walkthrough - HubSpot change to external CRM

Scenario: when a company is updated in HubSpot, push the change to an external CRM via its REST API.

Full config, sources included: _examples/outbound-hubspot-journal-to-api.json.

Step 1 - Declare the EventSources

Two sources are needed: a trigger source that polls HubSpot, and a target source that describes the external API. Both are entries of the config’s sources key, published together with the flow that uses them:

{ "sources": [ {"name": "hubspot_journal", "type": "hubspot", "config": {}}, { "name": "crm_api", "type": "rest", "config": { "outbound": { "base_url": "https://api.example-crm.com", "credential_name": "crm_api_key", "auth_type": "bearer" } } } ] }

Trigger - the HubSpot journal source (type: "hubspot", config: {}). The HubSpot journal v4 API is polled every 30 seconds, and the read position is persisted so no events are missed or replayed. See event-sources.md for the enriched payload shape.

Target - the external CRM REST source (type: "rest"). credential_name must match the name of an existing credential in your org (create credentials via the dashboard under Settings -> Credentials); the publish is refused until that credential exists. At runtime the credential is resolved and the appropriate Authorization header is injected on every outbound request. See event-sources.md for the full config.outbound field reference.

Step 2 - Write the flow

{ "name": "hubspot_company_to_api", "source": "hubspot_journal", "event_type": "0-2.update", "steps": [ { "id": "push_api", "action": "post", "target": "crm_api", "entity": "company", "url": "/api/companies/upsert", "body": "$.payload.object.properties" } ] }

event_type: "0-2.update" uses the HubSpot journal format {objectTypeId}.{action} lowercased. 0-2 is the HubSpot object type ID for companies; update is the action emitted by the journal. Other examples: 0-1.create (contact created), 0-3.update (deal updated), 0-2.delete (company deleted); the action is always one of create, update or delete. See event-sources.md for the full event type table.

body: "$.payload.object.properties" sends the HubSpot property bag to the CRM API. The url is appended to base_url, so the actual request goes to https://api.example-crm.com/api/companies/upsert. This is the explicit-body pattern: body always wins over any crm_api schema on the company entity, so none is declared here. Omit body instead (keep entity) to have post project a entities.<entity>.schemas.<target>.field_mappings list into the outbound body automatically - see step-actions.md#body-projection. Declaring field_mappings on a non-hubspot schema that is neither a body-less post step’s target nor a transform step’s from_schema has zero runtime consumers; see entities.md for the exhaustive list of what reads what.

Step 3 - Deploy and publish

The body is sent flat, same note as step 4 of the inbound walkthrough above - no {"config": ...} wrapper (that shape is PATCH /draft’s legacy patch wrapper only, never config; see ../config-v3/draft-publish.md#legacy-wrapper-deprecated).

curl -X PATCH https://app.plugsync.com/api/connectors/<connector_id>/draft \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "version": "3.0", "entities": [ { "name": "company", "schemas": { "hubspot": { "object_type": "companies", "identity": {"strategy": "match_or_create", "property": "hs_tax_id"}, "field_mappings": [ {"source": "$.payload.object.properties.name", "target": "name"} ] } } } ], "flows": [ { "name": "hubspot_company_to_api", "source": "hubspot_journal", "event_type": "0-2.update", "steps": [ {"id": "push_api", "action": "post", "target": "crm_api", "entity": "company", "url": "/api/companies/upsert", "body": "$.payload.object.properties"} ] } ], "sources": [ {"name": "hubspot_journal", "type": "hubspot", "config": {}}, { "name": "crm_api", "type": "rest", "config": { "outbound": { "base_url": "https://api.example-crm.com", "credential_name": "crm_api_key", "auth_type": "bearer" } } } ], "settings": {} }'

Then publish from the dashboard: “Review changes” -> “Publish v1”. That publish creates both sources.


C. Choosing an action

Pick the action by intent; follow the link for the field-level signature.

Your intentActionField reference
Create if missing, update if presentupsertstep-actions.md
Insert onlycreatestep-actions.md
Modify an existing record onlyupdatestep-actions.md
Remove a recorddeletestep-actions.md
Reshape the payload into $.canonicaltransformstep-actions.md
Look up a target record before actingfindstep-actions.md
Push to an external REST APIpoststep-actions.md
Best-effort side notification (must not abort the flow)notifystep-actions.md
Custom code (HTTP, branching, state)plugin (see the Plugin runtime guide)step-actions.md
Conditionally do nothing / abort a branchskip + whenstep-actions.md
Link two already-upserted objects one pair at a timeassociatestep-actions.md
High-volume batch write to HubSpot (v3.1)upsert_many / create_many / update_many / delete_manystep-actions.md
Bulk-create associations from pre-built pairs (v3.1)associate_manystep-actions.md

A typo in a step field saves, then warns. A step accepts keys it does not know, so that you can attach your own annotations to it: a misspelled field name therefore saves to the draft without error, and the engine ignores it at runtime. The publish preview and the publish itself report it as an unknown_step_key warning, with a “did you mean” suggestion when a known field is close and the list of fields the action reads. A field that exists but belongs to another action (for example fields on a step that does not take it) is refused when the draft is saved, or reported as a warning at publish for the fields that have no save-time check. A warning does not block the publish, so read the warnings before you publish and copy field names exactly from the reference above.

Last updated on