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: _examples/inbound-webhook-to-hubspot.json + _examples/inbound-webhook-to-hubspot.sources.json.

Step 1 - Create the EventSource

EventSources are not part of the config blob. Create shop_webhook first; the flow’s source field will reference it by name. See event-sources.md for the full API and field reference.

curl -X POST https://app.plugsync.com/api/connectors/<connector_id>/sources \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "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" } }, "secret": "<your-hmac-secret>" }'

The secret is stored encrypted and never returned by the API. Rotate it later with POST .../sources/shop_webhook/rotate-secret.

Step 2 - Define the entity

The company entity has a single hubspot schema. Because there is exactly one schema and it is named hubspot, infer_entity_mode 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.strategy: "match_or_create" with property: "hs_tax_id" means HubSpot looks up by tax ID; if no company matches, one is created. 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 SQS envelope as event_type before the message reaches flow_worker. 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 exact-match-wins precedence rule.

Step 4 - Deploy and publish

Save the full config (entities + flows + 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"} ] } ], "settings": {} }'

Then open the dashboard, click “Review changes”, and publish as v1. Workers pick up the new config within seconds via the outbox dispatcher.


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: _examples/outbound-hubspot-journal-to-api.json + _examples/outbound-hubspot-journal-to-api.sources.json.

Step 1 - Create the EventSources

Two sources are needed: a trigger source that polls HubSpot, and a target source that describes the external API.

Trigger - the HubSpot journal source (type: "hubspot", config: {}):

curl -X POST https://app.plugsync.com/api/connectors/<connector_id>/sources \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "name": "hubspot_journal", "type": "hubspot", "config": {} }'

source_worker polls the HubSpot journal v4 API every 30 seconds and persists its read position so no events are missed or replayed. See event-sources.md for the enriched payload shape.

Target - the external CRM REST source (type: "rest"):

curl -X POST https://app.plugsync.com/api/connectors/<connector_id>/sources \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "name": "crm_api", "type": "rest", "config": { "outbound": { "base_url": "https://api.example-crm.com", "credential_name": "crm_api_key", "auth_type": "bearer" } } }'

credential_name must match the name of an existing credential in your org (create credentials via the dashboard under Settings -> Credentials). At runtime flow_worker resolves the credential and injects the appropriate Authorization header 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.created (contact created), 0-3.modified (deal modified). 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"} ] } ], "settings": {} }'

Then publish from the dashboard: “Review changes” -> “Publish v1”.


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

Typos pass silently. StepV3 allows unknown fields (extra="allow"), so a typo in a step field passes draft-save without error. Copy field names exactly from the reference above; errors only surface at runtime.

Last updated on