One-Time CRM Setup
Use this pattern for app templates that require CRM configuration before the app can work properly, such as custom properties, property groups and pipelines.
When to use it
Section titled “When to use it”Use one-time CRM setup when a template:
- needs custom CRM properties or a pipeline to exist before the UI or serverless logic can run successfully
- needs those changes to be repeatable across portals
- benefits from an install-time summary of required scopes and CRM changes
Do not use this pattern for templates that only need standard CRM scopes, saved config, or external API credentials.
Authoring contract
Section titled “Authoring contract”Add a setup object to the template’s tc-config.json:
{ "docs_url": "https://tradingcards.dev/docs/templates/example-template/", "setup": { "enabled": true, "requiredOptionalScopes": [], "crmChangesSummary": [ { "objectType": "example_object", "title": "Create property group", "description": "Creates the template-specific property group." } ], "steps": [ { "kind": "propertyGroup", "objectType": "example_object", "name": "example_template_object", "label": "Example Template", "displayOrder": -1 }, { "kind": "property", "objectType": "example_object", "groupName": "example_template_object", "name": "example_property", "label": "Example Property", "description": "Used by the template runtime.", "type": "string", "fieldType": "text" }, { "kind": "pipeline", "objectType": "deals", "name": "example_pipeline", "label": "Example Pipeline", "stages": [ { "label": "Open", "metadata": { "probability": "0.2" } }, { "label": "Won", "metadata": { "probability": "1.0" } }, { "label": "Lost", "metadata": { "probability": "0.0" } } ] } ] }}Step kinds
Section titled “Step kinds”Steps run in the order listed. Each step’s kind says what it creates:
| Kind | Creates | Recognised as existing by |
|---|---|---|
propertyGroup | A property group on objectType | objectType and name |
property | A property in groupName on objectType | objectType and name |
pipeline | A pipeline with its stages on objectType | objectType and label, ignoring case |
The discriminator is kind because a property step already uses type for the HubSpot property type. Stage metadata follows the Pipelines API: probability for deals, ticketState for tickets.
Templates written before steps existed declare propertyGroups and properties as two separate lists. Those lists still work, run before steps, and can be mixed with it.
- Reuse the template’s top-level
docs_urlfor the setup guide link. Do not add a second docs URL insidesetup. - Put the schema scopes setup needs in the app’s required scopes and leave
requiredOptionalScopesempty, as Custom Deal CPQ does. The runner cannot yet detect which optional scopes are granted, so any scope listed there blocks setup. - Keep
stepsdeclarative so the setup runner can execute without template-specific branching. - List a property group before the properties that use it.
- Setup never changes a resource that already exists. Adding an option to a property or a stage to a pipeline in
tc-config.jsononly affects portals that have not run setup yet. - Put any custom properties created by setup into a property group named for the template.
- Use the template name for the property group display label.
- Use a stable slug derived from
project_idplus object type for the property group internal name.
UI pattern
Section titled “UI pattern”Templates that set setup.enabled to true should expose a setup entry point on the Settings page.
The setup modal should show:
- a confirmation message
- the required optional scopes and whether they are currently enabled
- a summary of the CRM changes that will be made
- a
Run setupbutton - a
View setup guidebutton
Run setup should be blocked when the required optional scopes are missing. When the template lists none, skip the scope check and its section of the modal.
Serverless pattern
Section titled “Serverless pattern”Implement two functions, *_get_setup_status and *_run_setup, on top of the shared runner in @tradingcards-dev/setup. Add it to the functions package.json and, if the template bundles its functions, mark it external.
const { getSetupStatus } = require("@tradingcards-dev/setup");const templateConfig = require("../../../tc-config.json");
exports.main = async () => { try { return { success: true, ...(await getSetupStatus(templateConfig)) }; } catch (error) { return { success: false, error: error.message || "Unable to load setup status" }; }};const { formatSetupFailures, runSetup } = require("@tradingcards-dev/setup");const templateConfig = require("../../../tc-config.json");
exports.main = async () => { try { const result = await runSetup(templateConfig); if (result.failed.length > 0) { return { success: false, error: formatSetupFailures(result.failed), result }; } return { success: true, result }; } catch (error) { return { success: false, error: error.message || "Unable to run setup" }; }};getSetupStatus:
- reads the template
setupmetadata and reuses the top-leveldocs_url - reports each step and whether its resource already exists, in
stepsandresourceStatus - reports
requiredOptionalScopesinscopeStatus
runSetup:
- stops before making changes if
requiredOptionalScopeslists anything - creates each missing resource in step order
- treats already-existing resources as a successful skip
- records a failed step with HubSpot’s error and carries on with the rest
- returns one entry per step with its outcome, including the ids HubSpot assigned to a new pipeline and its stages
A function that bundles tc-config.json must be re-bundled whenever the setup block changes, or the deployed function keeps running the old list.
Idempotency
Section titled “Idempotency”The setup runner must be safe to re-run. Existing resources should not cause failure, and reruns should clearly report what was created versus skipped.