Recipe: Channel Partners, Matched and Plated at the Table
Share this post
Recipe: Channel Partners, Matched and Plated at the Table
Yield: Partner assignments that live in HubSpot, not in someone's head
Prep time: A few hours to configure, plus HubSpot UI Extensions setup
Active time: One click per deal
Difficulty: Intermediate — two moving parts (Cloud Function + HubSpot UI card) that need to trust each other
Ingredients
Google Cloud
- Cloud Functions (gen2), Node.js 20
- GCP Secret Manager (three secrets: HubSpot access token, client secret, OpenRouter key)
HubSpot
- Operations Hub (required for UI Extensions)
- Custom association labels — one for partner company, one for partner contact
- Company properties identifying partner status, tier, type, and routing mode
- Contact properties identifying routing eligibility
Configuration
config.example.js— all instance-specific values in one place (see below)
Method
1. Define your partner data model in HubSpot
Before writing a line of code, the data model needs to be right. The router depends on company and contact properties that identify:
- Which companies are partners — a property (or multi-select) that marks a company as a partner type
- Whether the partnership is active — a status property distinguishing current from former partners
- What kind of partner they are — type property (Reseller, MSP, Distribution, Alliance, etc.)
- Their tier — if you use tiering (Elite, Gold, Platinum, etc.)
- Their routing mode — Single (one designated rep) or Multi (any active partner contact)
On the contact side, you need a way to identify the designated routing rep for Single-mode partners, and a status/lifecycle combination that identifies active contacts for Multi-mode partners.
None of these property names or values are hardcoded to a specific convention — they're all configured in config.example.js.
2. Create custom association labels
The router writes two associations on deal assignment:
- Deal → Partner Company (a custom label, not the default company association)
- Deal → Partner Contact (a custom label for the channel rep)
Create these in HubSpot under Settings → Objects → [Object] → Associations. Each label gets a typeId — note these down, they go into config.example.js.
Using custom labels keeps partner associations distinct from the end-customer associations already on the deal, which matters for filtering and reporting downstream.
3. Configure config.example.js
Copy config.example.js to config.js and fill in every value:
- Cloud Function URL (your GCP project ID)
- Association type IDs for both labels
- Property names and filter values for partner eligibility
- Contact routing property names and values
- Scoring weights (adjust to reflect what matters most in your program)
- Country list and partner type options for the UI dropdowns
Every value in this file maps to a specific location in cloud/index.js or partner-recommender.jsx. Comments in the file point to each location.
4. Deploy the Cloud Function
The function handles four actions from the UI card:
recommend → scores all eligible partners and returns a ranked list
assign → creates the custom association labels on the deal
findContacts → returns eligible reps for a selected partner company
getPropertyOptions → fetches partner type options dynamically for the UI
All requests are authenticated via HubSpot v3 signature validation (HMAC-SHA256 on method + URL + body + timestamp). The client secret is stored in GCP Secret Manager and never exposed.
Deploy from cloud/ using cloudbuild.yaml or the gcloud CLI command in the README. Set the three secrets in Secret Manager before deploying.
5. Deploy the HubSpot UI card
The card is a standard HubSpot UI Extensions project. Before deploying:
- Replace
YOUR_GCP_PROJECT_IDinapp-hsmeta.json(permittedUrls) and in allhubspot.fetch()calls inpartner-recommender.jsx - Update
app-hsmeta.jsonwith your support email - Deploy:
hs project uploadfrom thehsproject/directory - Add the card to deal record views in HubSpot: Settings → Objects → Deals → Record customization
6. Verify end-to-end
Open a deal that has an associated end-customer company. Open the Partner Router card. Click Find Partners — you should see a scored list of eligible partners. Select one, check that contacts load correctly, and assign. Verify the association labels appear on the deal record with the correct label names.
Plating Notes
The scoring engine is transparent by design. Each partner row in the results table shows the reasons it scored the way it did — reps can see "Same country: Germany" and "Matches preferred type: Reseller" rather than just a number. This matters for adoption: people trust a recommendation they can read, not one that comes out of a black box.
Single vs Multi routing mode reflects how different partners actually want to work. Some have one person who owns all inbound deals; others have a whole team. The router handles both cases with a single property flag on the partner company record.
Country normalization happens at scoring time, not at filter time. Filtering by country would miss partners whose country is stored as "USA" when the customer is "United States." By normalizing both values before comparing, the router catches these matches without losing any candidates.
The LLM re-ranking placeholder (llmTop: [] in the API response) is intentional. The rules engine handles the structural matching well. The next layer — understanding which partner has domain expertise in a specific vertical, or a relationship history with a particular type of customer — is a problem for an AI that knows the partner network, not a scoring table. That connection is planned.
github.com/Suixcity/partner-router
Part of a professional portfolio — view the project brief