This is the exact workflow from a HubSpot custom CRM card: the rep opens the form from inside the deal, the form is already filled with that deal's details, it has conditional logic, and on save it updates the same deal instead of creating a new record. There is also a client-fillable link version.
Nothing to install. Click through it below the way a rep would during a call.
The card in the right column is the custom part. Everything else is a normal HubSpot deal.
Off-the-shelf form tools connect to a deal after submission and match records by email, which is why they can create duplicates and cannot pre-fill from the deal. A CRM card runs the other direction.
This is a real HubSpot developer-projects CRM card, not a mockup. A HubSpot admin runs hs project upload and it appears in the deal sidebar. Two pieces do the work.
// Renders in the deal sidebar. HubSpot passes the deal id in context, // so the form is filled from THIS deal before it opens. import React, { useState, useEffect } from 'react'; import { Button, Form, Select, Input, Text, hubspot } from '@hubspot/ui-extensions'; hubspot.extend(({ context }) => <ScopingCard dealId={context.crm.objectId} />); function ScopingCard({ dealId }) { const [deal, setDeal] = useState(null); const [need, setNeed] = useState(''); useEffect(() => { // load this deal's live values hubspot.serverless('getDeal', { propertiesToSend: ['hs_object_id', 'dealname', 'amount'] }) .then(setDeal); }, []); if (!deal) return <Text>Loading this deal...</Text>; const save = (v) => hubspot.serverless('saveScoping', { propertiesToSend: ['hs_object_id'], parameters: { ...v, need } }); return ( <Form onSubmit={save}> <Text>{deal.dealname} · ${deal.amount}</Text> <Select name="need" label="Primary need" required onChange={setNeed} options={[{label:'Fleet upfitting',value:'fleet'}, {label:'Single vehicle',value:'single'}]} /> {need === 'fleet' && <Input name="vehicles" label="How many vehicles?" />} {need === 'single' && <Input name="model" label="Make and model" />} <Button type="submit" variant="primary">Save to deal</Button> </Form> ); }
// PATCHes the SAME deal id. No new record, no email matching. const hubspot = require('@hubspot/api-client'); exports.main = async (context = {}) => { const dealId = context.propertiesToSend.hs_object_id; const { need, vehicles, model, timeline, notes } = context.parameters; const client = new hubspot.Client({ accessToken: process.env['PRIVATE_APP_ACCESS_TOKEN'] }); await client.crm.deals.basicApi.update(dealId, { properties: { scoping_status: 'completed', scoping_need: need, scoping_vehicles: vehicles || '', scoping_model: model || '', scoping_timeline: timeline || '', scoping_notes: notes || '', scoping_updated: new Date().toISOString() } }); return { ok: true, dealId }; };
The client link version is the same form served as a standalone page. It reads the deal id from the URL and calls the same save function, so the rep card and the client link both write to one record.
⇩ Download the full extension sourceBuilt by an independent developer as a working example for the r/hubspot thread about opening a pre-filled form from the deal. Sample data only.
If it is useful, it can be adapted to your real fields and pipeline and installed on your own HubSpot.