Skip to Content
ReferenceSchema provisioningOverview

HubSpot schema provisioning

plugsync provisions the HubSpot schema your connector config references: custom properties, property groups, and custom object schemas are created on your portal automatically. You do not pre-create anything in HubSpot, with four explicit exceptions listed in What is never created below.

Provisioning runs through four mechanisms, all backed by the same idempotent engine — creating something that already exists is always a no-op:

MechanismWhen it runsFailure behavior
PublishEvery publish, synchronouslyPublish fails with an actionable 422; no new version is created, but properties already created in this run remain on the portal — see Failure behavior
Portal connect / rebindBinding a connector to a (new) portalBest-effort: logged, never blocks the bind
One-click propertyOn demand from the mapping editor422 with the same actionable message
Enum option auto-appendAt runtime, before each HubSpot writeBest-effort: logged, the write proceeds

1. At publish (synchronous)

When you publish a connector config, plugsync walks every entity that has a schemas.hubspot block with an object_type and creates whatever is missing on the bound portal, before the new version is persisted:

  • Property groups — created properties are placed in a dedicated group so they are visible as “created by plugsync” and enumerable for a later cleanup. The group defaults to plugsync (label Plugsync) and is configurable per connector — see Property group and prefix. An entity’s schemas.hubspot.property_group still overrides the group for that entity.
  • Custom properties — see Which properties get provisioned.
  • Custom object schemas — an object_type of the fully-qualified custom form p<portalId>_<name> that does not exist on the portal yet is created on the fly, with the config’s custom properties embedded in the schema (first custom property becomes the primary display property). This requires the crm.schemas.custom.write scope — see Custom object schemas and the schema scope.
  • Pipelines and stages declared in the top-level pipelines block — see Pipelines declared in pipelines.

Failure behavior

Provisioning at publish is not atomic on the portal: plugsync creates properties one at a time, and a failure partway through a run leaves whatever was already created in place — there is no automatic cleanup of a failed run’s partial state. What IS atomic is the version: any failure surfaces as a 422 on the publish call, and no new version is created — your draft is untouched.

A failed entity does not stop the run: the entities and pipelines declared after it are still provisioned. The 422 detail therefore carries two lists, and only the two together describe what the run left on the portal:

ListEntries
errorsThe entities (and pipelines) that failed. Each entry carries the actionable error message plus the partial state below.
provisionedThe entities and pipelines that completed in this run, with the same entity / object_type / created / skipped_existing fields and no error.

Every entry, in either list, carries:

FieldMeaning
entityThe entity name, or pipeline:<label> for a pipeline.
object_typeThe HubSpot object type the entry provisioned against.
createdNames this run created on the portal: property names for an entity, the pipeline label for a pipeline. They stay live, in the configured property group — nothing removes them automatically. For a failed entity this covers what landed before the failing property.
skipped_existingNames this run found already on the portal (left untouched, as always).

Fix the cause and publish again: the retry re-derives the same property list, finds the created names already on the portal, and treats them as an idempotent no-op — it resumes from the property that failed, it does not recreate anything. Raw HubSpot transport errors never leak into the message. The other errors of POST /publish are in the API error catalog.

Preflight before the first write to HubSpot

Before provisioning creates anything on the portal, plugsync reads the properties each object type already has and validates only the properties this publish is about to create against HubSpot’s own vocabulary: an unrecognized property type, an incoherent type/field-type pairing, or an enumeration declared with no options (see below) all fail publish immediately, before the first write — these are configuration mistakes, not partial provisioning runs, so nothing about the created/skipped_existing shape above applies to them.

The check is scoped to what would be created because that is all it can protect: a property that already exists on the portal is never created or modified by publish, so its declared type or options cannot fail provisioning, and a mapping that provisions nothing (a JSONPath target, a target_template without provision_names) is not a property at all. Two more cases fall outside it for the same reason: a reserved-prefix target, which is a portal prerequisite and is never created (a missing one fails with the prerequisite error naming the portal feature, not with a spec error), and a target another entity of the same publish is already creating on the same object type, which the second entity finds already there. In practice a configuration that published before these checks existed keeps publishing as long as its properties are already live; the same declared specs on a property that does not exist yet are rejected. With schema provisioning disabled for your deployment nothing is created, so nothing is checked.

The publish preview shows the same findings as warnings for the whole derived property list, existing or not: the preview does not read the portal, so it cannot tell which properties publish would actually create.

Which properties get provisioned

For each entity with a schemas.hubspot block, the property list is derived from the config:

Config elementProvisioned as
identity.property (when set)A property with that name, string / text unless a mapping declares it (see below)
Each field_mappings[].targetA property named target
A mapping with target_template instead of targetEvery name listed in its provision_names. A template without provision_names is invisible to provisioning — declare the names it can expand to

Type and appearance default to HubSpot type string with field type text; a mapping can override them per-property with hubspot_type, hubspot_field_type, and hubspot_label. Targets that are JSONPath expressions ($.canonical.* and friends) are canonical-side mappings, not HubSpot writes, and are skipped.

The identity property’s string / text is a default, not a declaration. When a field_mappings entry targets the same name as identity.property, one property is provisioned and that mapping declares it: its hubspot_type, hubspot_field_type, hubspot_label and hubspot_options are the ones used. So an identity property you also map as an enumeration is created as a dropdown with its options, and preflight checks it like any other mapping.

hubspot_type: enumeration defaults its field type to select (not text) and requires a hubspot_options list — [{value, label}], label optional and left out of the saved config when you omit it, in which case the property is created with value as its label — declaring the dropdown’s values. HubSpot rejects creating an enumeration property with no options, so a mapping missing them fails preflight with an actionable error before publish writes anything to HubSpot, as long as the property does not exist on the portal yet (see Preflight before the first write to HubSpot):

{ "source": "$.payload.status", "target": "deal_status", "hubspot_type": "enumeration", "hubspot_options": [ {"value": "open", "label": "Open"}, {"value": "won", "label": "Won"}, {"value": "lost", "label": "Lost"} ] }

Provisioning is idempotent: properties that already exist on the portal are left exactly as they are (their type, label, and options are never modified) — adding a value to hubspot_options after the property already exists on the portal has no effect at publish; new values reaching HubSpot writes are still picked up additively by enum option auto-append.

Property group and prefix

The properties plugsync creates are made transparent and enumerable by two knobs, set once per connector under settings.hubspot_provisioning:

{ "settings": { "hubspot_provisioning": { "property_group": "plugsync", "property_group_label": "Plugsync", "property_prefix": "plugsync_" } } }
KnobDefaultMeaning
property_groupplugsyncInternal name of the dedicated group the created properties live under
property_group_labelPlugsyncThe group’s display label in HubSpot
property_prefixplugsync_Naming convention for the custom properties plugsync manages; together with the group it makes the managed set derivable

Every self-service connector gets the defaults with no configuration. The knobs exist mainly for enterprise / Custom-Dedicated connectors that want their own naming.

Both knobs can be changed freely — change property_group any time and the next publish creates / uses the new group; change property_prefix any time too, and the publish goes through. The prefix is a reserved naming convention: plugsync does not rewrite the internal names it provisions, so editing it never orphans an existing property and never creates a duplicate. It documents which names plugsync considers its own, and it is what lets a future uninstall enumerate the managed set.

Note: the prefix is a convention applied by naming, not a silent rewrite of your field_mappings[].target names. The flow runtime writes each mapping’s target to HubSpot verbatim, so a property’s internal name is whatever the config author writes; plugsync groups those properties, keeps the prefix stable, and never rewrites a target behind your back.

Pipelines declared in pipelines

Publish also creates the pipelines and stages declared in the connector’s top-level pipelines block, and the connect/rebind path below does the same on a best-effort basis. Pipeline provisioning is create-only and idempotent per label: a pipeline whose label already exists on the portal is left untouched, with no update, no delete and no reconciliation of stages edited later on the portal. A failure, for example a credential without the pipeline write scope (crm.schemas.deals.write for a deals pipeline), makes the publish fail with a 422 and an actionable message, and no new config version is saved.

A pipeline or stage ID that your config references directly, without declaring the pipeline in pipelines, is not created: it must already exist on the portal. The dashboard’s mapping editor discovers the portal’s pipelines and stages (GET /api/connectors/{connector_id}/hubspot-pipelines/{object_type}) so you can pick valid IDs instead of typing them. The same goes for association types: GET /api/connectors/{connector_id}/hubspot-association-types/{from_object_type}/{to_object_type} lists the type IDs the portal defines between two object types. Both accept ?credential_id= to read a portal the connector is not bound to yet (see Checking the target portal first).

2. At portal connect / rebind (best-effort)

Binding a connector to a portal re-runs provisioning of the active published config against that portal. This covers:

  • the HubSpot OAuth callback (connecting a portal),
  • re-binding a connector to a different credential (PATCH of the connector’s credential_id),
  • the AI agent’s bind_portal tool.

Connect a brand-new portal to an already-published connector and its custom properties materialize there without a re-publish. Differences from the publish-time path:

  • Best-effort: failures are logged and swallowed — a connect/rebind is never broken by a HubSpot error. The publish-time path is the strict one.
  • Best-effort per entity: the preflight that stops a whole publish is scoped to a single entity here. A config published while another portal was bound can declare a property spec HubSpot would reject and still have published, because the property already existed on the old portal and was therefore never created. On the new portal that property does not exist, so the declaration finally matters — and it costs you only its own entity: the other entities and the pipelines are still provisioned, and the failed one is reported with the same errors / provisioned entries as the publish failure behavior. Fix the mapping and re-bind (or publish) to provision it.
  • No-op without an active version: a connector that has never been published has nothing to provision yet; the first publish handles it.
  • Idempotent: re-binding an already-provisioned portal is a cheap no-op.

3. One-click property from the mapping editor

A source field with no matching HubSpot property can be provisioned without leaving the dashboard: the mapping editor’s property picker offers to create it on the spot. The underlying endpoint:

POST /api/connectors/{connector_id}/hubspot-properties/{object_type}
Body fieldRequiredDefaultNotes
nameyes—HubSpot internal property name
labelnoTitle Case of nameDisplay label
typenostringHubSpot property type
fieldTypenotextHubSpot field type
group_namenoplugsyncProperty group (created if absent)

The property group comes from group_name alone: this endpoint does not read the connector’s settings.hubspot_provisioning.property_group, nor an entity’s property_group. The mapping editor sends only name, so a property created with one click always lands in the plugsync group, even on a connector configured with another group. To keep it in your configured group, call the endpoint with group_name set, or declare the property in the mapping and let the publish create it (the publish uses the configured group).

Returns 201 with {"name": ..., "created": true} — or created: false when the property already existed (idempotent no-op). Failures return the same actionable 422 messages as publish. This is the same provisioning engine the publish and connect/rebind paths use, not a second implementation.

4. Enum option auto-append at runtime

When a flow writes a value to a HubSpot enumeration property whose option list does not contain that value yet, the engine appends the option (one HubSpot Schema API PATCH) before writing. No config field enables it; it is keyed on the target property being an enumeration. It is tightly gated:

  • enumeration-type properties only;
  • never on read-only, calculated, or externally-managed (externalOptions, e.g. owner fields) properties;
  • additive only: existing options are never removed or reordered;
  • best-effort: a failed append (e.g. a missing schema scope) is logged and the value is written as-is, exactly as without the feature.

Full behavior, gates, and the propagation caveat are documented in the Config V3 entities reference.

What is never created

Four limits are by design. Everything else your config references on the HubSpot side is provisioned for you.

Forms

plugsync never creates HubSpot forms. The submit_form step action writes submissions to an existing form, addressed by its form_guid, via the Forms v3 secure submissions API. Form creation would require marketing scopes the plugsync app deliberately does not request. Create the form in HubSpot and reference its GUID.

Reserved property prefixes: hs_* and a<appId>_*

HubSpot reserves two property-name prefixes that its create API rejects outright:

  • hs_* — HubSpot-managed properties (e.g. hs_price_eur);
  • a<appId>_* — integrator-managed properties owned by another app (e.g. a38567592_synced_at).

Mapping to one of these is fine when the property exists on the portal: provisioning treats reserved-prefix targets as prerequisites, verifies them, and never attempts to create them. If one is missing, publish fails with an error naming the property and, where known, the portal feature that materializes it — e.g. a missing hs_price_eur means multicurrency for EUR is not enabled on the portal. Enable the missing portal feature (or install the owning app), or remove the mapping.

Custom object schemas and the schema scope

Creating a custom object schema (as opposed to properties on an existing one) requires the crm.schemas.custom.write scope on the connector’s HubSpot credential. Two cases:

  • Standard OAuth connection (plugsync public app): HubSpot does not allow public apps to request crm.schemas.custom.write — it is a private-app-only scope. Publishing a config that targets a custom object whose schema does not exist yet fails with an actionable error: create the custom object schema on the portal once (Settings -> Objects -> Custom objects), then publish again. From then on, its properties are provisioned normally (crm.objects.custom.* and crm.schemas.custom.read are requested when your connectors have an entity on a custom object; see Connect HubSpot for how the scopes of a feature are granted).
  • Private app token credential: include crm.schemas.custom.write in the token’s scopes and plugsync creates the schema at publish, custom properties embedded.

Association definitions involving a custom object

An associate step can only link two object types for which the portal holds an association definition. Between two standard objects HubSpot ships its own, so those always work. plugsync never creates one, and creating a custom object does not create any either: between two custom objects the definition does not exist until you create it.

While it is missing, every association between the two object types is rejected, and HubSpot’s own rejection message names neither object. plugsync reports the situation on both sides:

  • Before publish — the preflight (GET /api/connectors/{connector_id}/preflight, the dashboard’s Review-changes drawer, plugsync validate) reads the portal’s association definitions for every associate step whose endpoints include a custom object, and reports the error association_definition_missing, naming the two object types, when the pair has none. A portal it cannot read is reported as an association_types_unverified warning instead: an unreadable portal is not evidence of a missing definition.
  • At runtime — the association fails with no association definition between <from> and <to> on the portal: create the association labels between the two objects (custom objects get none by default), then replay the event, on the event in the dashboard. The failure is permanent, so once the definition exists the affected events have to be replayed.

Create the definition once, on the portal:

  • In HubSpot: Settings -> Data Management -> Objects -> the custom object -> Associations -> create an association to the other object.
  • Via the HubSpot API: POST /crm/v4/associations/{fromObjectType}/{toObjectType}/labels.

Nothing else has to change: the association type is resolved against the portal on each write, so the next event associates normally without a republish.

Do I need to pre-create anything on the portal?

No, except the four cases above. Recap:

Your config referencesplugsync creates it?
A custom property (any non-reserved name)Yes — at publish, rebind, or one-click
A property groupYes
A custom object schema (p<portalId>_* object type)Yes with a credential granting crm.schemas.custom.write (private app token); otherwise create the schema once on the portal, its properties are then provisioned normally
A new option value on an enumeration propertyYes — appended at runtime, additive
An hs_* or a<appId>_* propertyNever — prerequisite: enable the portal feature / owning app
A pipeline and its stages declared in pipelinesYes — at publish or rebind, create-only (an existing label is left untouched)
A pipeline or stage ID referenced directly, not declared in pipelinesNo — create it on the portal, pick it from the dashboard
A form (submit_form)Never — create the form, reference its GUID
An association definition between two object types (associate)Never — HubSpot ships those between standard objects; between two custom objects create it once on the portal
Last updated on