1 · When we create a class
One Course record per dated session. Set the logistics once; automation does the rest.
flowchart TD A([fa:fa-calendar-days New class scheduled]):::start --> B["Create a Course record
one Course = one dated session"]:::obj B --> C["Set Course properties once:
cohort code · dates · venue (TBA ok) · hotel · trainer · capacity"]:::obj C --> D["Associate registrants as Contacts
label = Registered (many-to-many)"]:::step D --> E{Venue confirmed?}:::dec E -- "still TBA" --> D E -- "TBA → real venue" --> F["fa:fa-gear Workflow: copy Course venue/date
→ all associated contacts"]:::wf F --> G["fa:fa-envelope Workflow: send 'Venue confirmed'
marketing email to all attendees"]:::email G --> H["Advance stage; mark Attended after the session"]:::step classDef start fill:#f2f3f4,stroke:#cdd3d9,color:#232b33; classDef obj fill:#FDEBE8,stroke:#EF4C32,color:#a3301c; classDef email fill:#FDEBE8,stroke:#EF4C32,color:#a3301c; classDef wf fill:#eef0f2,stroke:#6b7682,color:#3f4852; classDef step fill:#fff,stroke:#cdd3d9,color:#232b33; classDef dec fill:#fff5ee,stroke:#EB5825,color:#8a3d1e;
2 · Backfilling everyone's prior classes
Consolidate historical attendance into one file; HubSpot builds the Course records + associations in a single import.
flowchart TD S1["Source A, contact properties
course_name + date + venue
~101 contacts already tagged"]:::src --> M S2["Source B, static lists / CSV imports
older cohorts (TLG, CoP, etc.)"]:::src --> M M["Consolidate → one spreadsheet
1 row per attendee"]:::step --> K["Add a Cohort Code column
repeated for every attendee in that session"]:::obj K --> I["fa:fa-inbox Multi-object import (Courses + Contacts)
matched on Cohort Code + email"]:::obj I --> R["HubSpot dedupes Courses by Cohort Code
→ 1 Course per cohort, attendees associated"]:::email R --> DONE([fa:fa-circle-check History backfilled
no per-record clicking]):::done classDef src fill:#fff,stroke:#cdd3d9,color:#232b33; classDef step fill:#fff,stroke:#cdd3d9,color:#232b33; classDef obj fill:#FDEBE8,stroke:#EF4C32,color:#a3301c; classDef email fill:#FDEBE8,stroke:#EF4C32,color:#a3301c; classDef done fill:#eef6f3,stroke:#1d7a5f,color:#1d7a5f;
A single multi-object import: not a workflow
HubSpot can create Course records and associate them to Contacts in the same import. One row per attendee with a shared Cohort Code; rows sharing a code collapse into one Course with many attendees. I can pre-build Source A (the ~101 already-tagged contacts) straight from the API into this sheet.
3 · The Course object schema to build
The object ships with the defaults below (0 custom yet). Add these custom properties so a Course record holds a whole session's logistics. (Fixes today's typos along the way, e.g. hotel_recommendations.)
| Property to add | Type | Holds |
|---|---|---|
cohort_code | Single-line text | Stable key, e.g. TFM-2026-04-22-MN (dedup key for import & reporting) |
program_code | Dropdown | TFM · HCS · HIJ · HSF · AES · MTOP · CoP … |
session_start_date / session_end_date | Date picker (date only, HubSpot has no time-of-day on dates) | The session dates |
session_times / day_2_times | Dropdown (preset options), mirrors your existing course_time | e.g. "9:00 AM – 4:00 PM", keeps times consistent & filterable |
daily_schedule | Multi-line text | Agenda detail |
venue_name / venue_address | Text / Multi-line | Where it's held |
parking_info / hotel_recommendations | Multi-line text | Logistics (typo corrected) |
lead_trainer / lead_trainer_email | Single-line text | Primary trainer |
trainer_2 / trainer_2_email | Single-line text | Second trainer |
delivery_mode | Dropdown | In-person / Virtual / Hybrid |
Use the built-in defaults too: hs_course_name (session label), hs_enrollment_capacity (seats), hs_pipeline_stage, Owner.
4 · Associations & pipeline
| Setting | Value |
|---|---|
| Course → Contact association labels | Interested · Registered · Attended · No-show · Cancelled (many-to-many, a contact can join many courses) |
| Pipeline stages (ships as Open / Closed) | Planning → Registration Open → Confirmed → Running → Complete (+ Cancelled) |
5 · Order of operations
| # | Step | Where |
|---|---|---|
| 1 | Add the custom Course properties above (incl. cohort_code) | Settings → Properties → Course |
| 2 | Add the association labels (Registered / Attended / No-show) & set the pipeline stages | Settings → Objects → Courses |
| 3 | Build the backfill spreadsheet (auto-export Source A + gather Source B) | Spreadsheet |
| 4 | Import Courses + Contacts, matched on Cohort Code + email | CRM → Import |
| 5 | Build the two workflows: copy-down (Course venue → contacts) and announcement email | Automation → Workflows |
| 6 | Going forward: every new class starts as a Course record (Flow 1) | , |
6 · Why this is the right call: what the authorities say
This is the model HubSpot and its partners recommend for courses, and, importantly, the Course object is part of the Object Library, which activates on your current plan (it is not a fully-custom object that would require Enterprise).
"Most teams tracking training… are doing it wrong. They're logging course completions as Deal stages, tracking certifications in a custom Contact property with 47 dropdown options…" The fix, HubSpot's Course object, was "released in late 2024 as part of their Object Library."
The recommended model is one record per session with attendees associated, "For an event with multiple sessions, a Session… object is a must-have."
Associate contacts to the course with association labels, "Interested, Enrolled, Completed", usable in segments, reporting and automation; many-to-many so one contact can join multiple courses.
To associate contacts with records on import, "import them both at the same time, with a common field column… and unique IDs… to prevent duplicates." Native import handles many-to-many awkwardly; tools like Insycle/Coefficient bulk-associate via email matching.