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 three explicit exceptions listed in What is never created below.
Provisioning runs through four mechanisms, all backed by the same idempotent engine ((internal plugsync tooling - see the source repo)) — creating something that already exists is always a no-op:
| Mechanism | When it runs | Failure behavior |
|---|---|---|
| Publish | Every publish, synchronously | Publish fails with an actionable 422; nothing goes live |
| Portal connect / rebind | Binding a connector to a (new) portal | Best-effort: logged, never blocks the bind |
| One-click property | On demand from the mapping editor | 422 with the same actionable message |
| Enum option auto-append | At runtime, before each HubSpot write | Best-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
(_provision_hubspot_schemas_sync in (internal plugsync tooling - see the source repo)):
- 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(labelPlugsync) and is configurable per connector — see Property group and prefix. An entity’sschemas.hubspot.property_groupstill overrides the group for that entity. - Custom properties — see Which properties get provisioned.
- Custom object schemas — an
object_typeof the fully-qualified custom formp<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 thecrm.schemas.custom.writescope — see Custom object schemas and the schema scope.
Provisioning at publish is atomic from your point of view: any failure
surfaces as a 422 on the publish call with one structured, user-actionable
error per failing entity ({entity, object_type, error}), and no new
version is created — your draft is untouched, fix the cause and publish
again. Raw HubSpot transport errors never leak into the message.
Which properties get provisioned
For each entity with a schemas.hubspot block, the property list is derived
from the config (_collect_required_hubspot_properties):
| Config element | Provisioned as |
|---|---|
identity.property (when the identity strategy uses a property) | A string / text property with that name |
Each field_mappings[].target | A property named target |
A mapping with target_template instead of target | Every name listed in its provision_names (issue #519). 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.
Provisioning is idempotent: properties that already exist on the portal are left exactly as they are (their type, label, and options are never modified).
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_"
}
}
}| Knob | Default | Meaning |
|---|---|---|
property_group | plugsync | Internal name of the dedicated group the created properties live under |
property_group_label | Plugsync | The group’s display label in HubSpot |
property_prefix | plugsync_ | 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 (decision on issue #1190,
option B) and feeds the primitive a future uninstall would use to 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 are not auto-created
Publish provisioning covers properties, groups, and custom object schemas.
It does not create pipelines or pipeline stages: a pipeline or stage ID
your config references 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.
2. At portal connect / rebind (best-effort)
Binding a connector to a portal re-runs provisioning of the active
published config against that portal
(reprovision_active_hubspot_schemas, PR #306). This covers:
- the HubSpot OAuth callback (connecting a portal),
- re-binding a connector to a different credential (
PATCHof the connector’scredential_id), - the AI agent’s
bind_portaltool.
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.
- 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 (issue #89): 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 field | Required | Default | Notes |
|---|---|---|---|
name | yes | — | HubSpot internal property name |
label | no | Title Case of name | Display label |
type | no | string | HubSpot property type |
fieldType | no | text | HubSpot field type |
group_name | no | plugsync | Property group (created if absent) |
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
SchemaProvisioner 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 ((internal plugsync tooling - see the source repo), issue #191). 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
Three 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 (submit_form in
(internal plugsync tooling - see the source repo), issue #489). 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, issue #418).
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.*andcrm.schemas.custom.readare part of the standard OAuth scope set). - Private app token credential: include
crm.schemas.custom.writein the token’s scopes and plugsync creates the schema at publish, custom properties embedded.
Do I need to pre-create anything on the portal?
No, except the three cases above. Recap:
| Your config references | plugsync creates it? |
|---|---|
| A custom property (any non-reserved name) | Yes — at publish, rebind, or one-click |
| A property group | Yes |
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 property | Yes — appended at runtime, additive |
An hs_* or a<appId>_* property | Never — prerequisite: enable the portal feature / owning app |
| A pipeline or pipeline stage | No — create it on the portal, pick it from the dashboard |
A form (submit_form) | Never — create the form, reference its GUID |