Deal scoping form card. Full HubSpot UI-extension source. Real, deployable HubSpot developer-projects structure (platformVersion 2025.1). A HubSpot admin runs 'hs project upload' from the folder holding hsproject.json. Sample data only. Field names map to whatever your real pipeline uses. ======================================================================== ======== extension/hsproject.json ======== { "name": "deal-scoping-card", "srcDir": "src", "platformVersion": "2025.1" } ======== extension/README.md ======== # Deal scoping form card (HubSpot UI extension) A HubSpot custom CRM card that opens a pre-filled form straight from a deal, runs conditional logic, and writes the answers back to the same deal. Built on HubSpot's developer projects platform (`platformVersion 2025.1`), so a HubSpot admin installs it with `hs project upload` and it appears in the deal sidebar. This is the real, deployable structure behind the interactive demo. It is written against a generic sales-scoping example (fleet upfitting). The field names, options, and conditional branches map one to one to whatever your real pipeline uses. ## What it does - **Deal to form direction.** The card runs inside the deal, so HubSpot hands it the deal id in `context.crm.objectId`. It reads the live deal via `getDeal` and fills the form before it opens. There is no post-submit matching and no chance of a duplicate record. - **Conditional logic.** Fields show or hide based on answers (the `need` select drives which follow-up field appears), so the rep only sees what is relevant. - **Writes back to the same deal.** On submit, `saveScoping` PATCHes the same deal id with `client.crm.deals.basicApi.update(dealId, ...)`. - **Rep view and client link, one record.** The rep opens it from the sidebar. The client-fill version is the same form served as a standalone page that carries the deal id in the link and calls the same `saveScoping` function, so both paths write to one deal. ## Files ``` hsproject.json project manifest src/app/app.json private app config (scopes, card reference) src/app/extensions/ scoping-card.json card definition (location: crm.record.sidebar, objectTypes: deals) ScopingCard.jsx the React card, pre-fills + conditional logic + submit package.json @hubspot/ui-extensions, react src/app/app.functions/ serverless.json registers getDeal + saveScoping getDeal.js reads live deal properties for the pre-fill saveScoping.js PATCHes the same deal on submit package.json @hubspot/api-client .env.sample where the private app token goes ``` ## Custom properties to create on the deal The write-back targets these deal properties. Create them once (Settings, Properties, Deal properties), or rename them in `saveScoping.js` to match properties you already have. - `scoping_status` (single-line text or dropdown) - `scoping_need` (dropdown) - `scoping_vehicles`, `scoping_model`, `scoping_parts` (single-line text) - `scoping_timeline` (dropdown) - `scoping_notes` (multi-line text) - `scoping_updated` (date or datetime) ## Install 1. `hs project upload` from the folder that holds `hsproject.json`. 2. Add the private app token as a secret: `hs secrets add PRIVATE_APP_ACCESS_TOKEN`. 3. Open any deal. The "Scoping call form" card is in the right sidebar. ## The client link version The standalone client form is a small hosted page that reads `?deal=` from the URL, loads the same read-only deal fields, collects the client's answers, and calls the same `saveScoping` function. It is described here rather than shipped as part of the card project because it lives outside HubSpot. The interactive demo shows both views. ======== extension/src/app/app.json ======== { "name": "Deal scoping form card", "description": "Opens a pre-filled scoping form straight from a deal, with conditional logic, and writes the answers back to the same deal.", "uid": "deal_scoping_form_app", "scopes": [ "crm.objects.deals.read", "crm.objects.deals.write", "crm.objects.contacts.read" ], "public": false, "extensions": { "crm": { "cards": [ { "file": "extensions/scoping-card.json" } ] } } } ======== extension/src/app/extensions/scoping-card.json ======== { "type": "crm-card", "data": { "title": "Scoping call form", "description": "Open a pre-filled scoping form from this deal. Answers save back to this deal.", "uid": "deal_scoping_form_card", "location": "crm.record.sidebar", "module": { "file": "ScopingCard.jsx" }, "objectTypes": [ { "name": "deals" } ] } } ======== extension/src/app/extensions/ScopingCard.jsx ======== import React, { useState, useEffect } from 'react'; import { Button, Flex, Form, Input, LoadingSpinner, Select, Text, TextArea, Alert, hubspot, } from '@hubspot/ui-extensions'; // HubSpot hands the card the current deal id in context, so the form is // filled from THIS deal before it opens. No email matching, no guessing. hubspot.extend(({ context }) => ); function ScopingCard({ dealId }) { const [deal, setDeal] = useState(null); const [need, setNeed] = useState(''); const [saved, setSaved] = useState(false); const [error, setError] = useState(''); // Load the live deal values when the card opens. useEffect(() => { hubspot .serverless('getDeal', { propertiesToSend: [ 'hs_object_id', 'dealname', 'amount', 'dealstage', 'scoping_status', 'scoping_need', ], }) .then((res) => { setDeal(res); setNeed(res.scoping_need || ''); }) .catch((e) => setError(e.message)); }, []); if (error) { return {error}; } if (!deal) { return ; } if (saved) { return ( The answers were written back to deal {deal.hs_object_id}. Refresh the record to see them. ); } const onSubmit = (values) => { hubspot .serverless('saveScoping', { propertiesToSend: ['hs_object_id'], parameters: { ...values, need }, }) .then(() => setSaved(true)) .catch((e) => setError(e.message)); }; return (
{deal.dealname} at {deal.amount ? `$${deal.amount}` : 'no amount set'}, stage {deal.dealstage}. )} {need === 'single' && ( )} {need === 'parts' && ( )}