Build documentation · sample runbook

CourseSync build documentation

A module by module runbook for the MindBody purchase to enrollment scenario: what each of the twenty modules does, the field mappings, how the flag-don't-fail paths work, and what the demo simplifies versus what a higher-volume production build should do. This is the sample of what full documentation with every build looks like.

← back to the walkthrough

Scope and honesty

This documents the blueprint built for the specification posted publicly on Make's Hire a Pro board, then updated to match the revised spec. It runs on invented sample data. No prior MindBody or StartIntegrate client build is claimed. The MindBody steps are written with generic HTTP so the blueprint imports into any Make account without the connector installed; each connector boundary notes where the native StartIntegrate action swaps in. In the revised design, MindBody is read only: the scenario checks whether a session was booked but never books, adds, or changes anything in MindBody.

Contents

1. Connections and prerequisites

ConnectionUsed byNotes
MindBody APITrigger, module 6Read only. A webhook subscription for clientSale.created, plus a raw call to GET /client/clientvisits (and GET /client/clients if the sale event omits the email). Three request headers on every call: Api-Key, SiteId, Authorization: Bearer with a staff token.
HubSpotModules 2, 7, 15, 16, 17, 20Private app token. Scopes needed: crm.objects.contacts.read and write, crm.lists.read and write, and marketing-email for the Single-Send API. Single-Send requires Marketing Hub Enterprise. Placeholder in the blueprint: <HUBSPOT_CONNECTION_ID>.
Google SheetsModule 18Access to the target spreadsheet with an Enrollments tab that includes the MB Enrollment column.
Gmail or emailModules 11, 13, 19Any send-capable connection. Used for the parent request (zero booking), the admin flag, and the VA alert. Swap for Slack or a task if preferred.
Segment resolverModule 8A small hosted endpoint that matches the segment by date-range containment, or the fully in-Make alternative described in section 7.

Values to supply before the scenario runs: the MindBody SiteId, the Google spreadsheet id, the HubSpot confirmation template id, the admin recipient, the VA recipient (a placeholder pending the hire), and the override property name (manual_enrollment_skip_automation).

2. Trigger setup and signature verification

The StartIntegrate connector has no purchase trigger, so the scenario is fired by a MindBody webhook, not a connector poll.

  1. In the MindBody Webhooks API, create a subscription for the event clientSale.created. Point its delivery URL at this scenario's custom webhook address.
  2. MindBody returns a messageSignatureKey for the subscription. Store it in the scenario.
  3. On every delivery, compute HMAC-SHA256 over the raw request body using that key and compare, as sha256=<hash>, against the X-Mindbody-Signature header. Reject the bundle if it does not match. A plain custom webhook does not verify the signature by itself, so a small verification shim gates on it in production.

The delivered envelope is { messageId, eventId, eventSchemaVersion, eventInstanceOriginationDateTime, eventData }. eventSchemaVersion is 1. eventData carries siteId, clientId, saleId, and the purchased items. Verified against the real MindBody sandbox: the live event may not carry the client email, so a GET /client/clients on the clientId resolves it before the HubSpot steps when it is absent.

3. Module by module reference (20 modules)

Nine modules run before a three-route router; the router then fans to the zero-booking, flag, and proceed paths. Routes are evaluated by filters, so exactly one path runs per bundle.

The twenty modules: the chain, the router, and the three routes 1 webhook 2 read contact 3 context vars 4 sleep 300s 5 sleep 300s 6 visits read 7 lists index 8 segment resolver 9 decision vars 10 · three-route router route A · zero booking -> ask parent, stop 11 email parent 12 stop route B · 0 or >1 match -> flag admin, stop 13 flag admin 14 stop route C · exactly 1 match -> dedup gate, steps 2-6 15 dedup read 16 upsert contact 17 add to list 18 sheet row 19 VA alert 20 confirm- ation

Module numbers match the reference table below. The gates named on each route are the filters that select it.

IDModulePurposeKey mappings
1gateway:CustomWebHook v1 Receive clientSale.created from MindBody. webhook + signature configured after import
2hubspotcrm:MakeAPICall v2 STEP 0: read the contact for the override and courses-taken. POST /crm/v3/objects/contacts/search by email; returns manual_enrollment_skip_automation, coursesync_courses_taken, id
3util:SetVariables v1 Context vars. override, contactId, contactEmail, coursesTaken, courseName, delivery
4util:FunctionSleep v1 STEP 1 wait, part 1 of 2. duration 300s (1s when override is set)
5util:FunctionSleep v1 STEP 1 wait, part 2 of 2 (Sleep maxes at 300s). duration 300s (1s when override is set)
6http:ActionSendData v3 STEP 1: read the client's forward visits (read only). GET /public/v6/client/clientvisits; keep AppointmentStatus = Booked; response at {{6.data.Visits}}
7hubspotcrm:MakeAPICall v2 Pull the HubSpot lists index for segment matching. POST /crm/v3/lists/search; MANUAL and SNAPSHOT lists; each with listId + name
8http:ActionSendData v3 Segment resolver: date-range containment match. POST session date + course tokens + lists index; returns matchCount, matchedListId, matchedListName
9util:SetVariables v1 Decision vars. bookingCount, matchCount, matchedListId, matchedListName, enrollmentMode, vaEligible
10builtin:BasicRouter v1 Route to zero-booking, flag, or proceed. three routes, selected by filters
11google-email:sendAnEmail v4 Zero booking: email the parent for dates. filter bookingCount = 0 AND override not true
12builtin:StopScenario v1 Stop (manual completion). no re-trigger; steps 2-6 do not run
13google-email:sendAnEmail v4 Flag: segment match is 0 or more than 1, notify the admin. filter matchCount not equal to 1 (with something to process)
14builtin:StopScenario v1 Stop (needs a human). nothing is written until a person resolves the segment
15hubspotcrm:MakeAPICall v2 Dedup: read the target segment's memberships. GET /crm/v3/lists/{{matchedListId}}/memberships; filter matchCount = 1
16hubspotcrm:upsertAContact v2 STEP 2: append the course to the multi-select. filter not already a member; coursesync_ properties only; courses_taken appended, not overwritten
17hubspotcrm:addMembersToAList v2 STEP 3: add the contact to the matched segment (native). listId {{matchedListId}}; recordIds ["{{16.id}}"]; needs crm.lists r/w
18google-sheets:addRow v2 STEP 4: append an audit row with the MB Enrollment column. Enrollments tab; Timestamp, Student, Course, Segment, MB Enrollment, SaleId
19google-email:sendAnEmail v4 STEP 5: VA alert, conditional. filter vaEligible = true (MS or HS SSS, in-person, summer)
20hubspotcrm:MakeAPICall v2 STEP 6: draft the confirmation to the admin. POST /marketing/v4/email/single-send; subject prefixed DRAFT COURSE CONF

4. The parts that carry the build

The wait, then a read-only booking check

Parents book their sessions a few minutes after buying, so the scenario waits about ten minutes before it looks (modules 4 and 5, two chained Sleeps, because a single Sleep maxes at 300 seconds). Module 6 then reads the client's forward visit window and asks a tolerant question: is any session booked for this course and week, not is session one booked, because parents book out of order. It keeps only future bookings whose AppointmentStatus is Booked, and carries that session's date forward. Two real-sandbox traps are folded into the filter: the status field is AppointmentStatus, not Status, and Missed is TRUE on future bookings, so booked is never inferred from Missed. When nothing is booked, that is a normal outcome, not an error, and the scenario takes the parent-email path.

Deduplication before any write

A parent can book several sessions of one course in one sitting, and each booking can fire the flow. Module 15 reads the target segment's current members, and module 16 only proceeds when the contact is not already in it. Static (manual or snapshot) list membership through the v3 Lists API is used precisely because it is immediate rather than lag prone. This is what makes a repeat delivery of the same purchase a safe no-op.

The date-range segment match

The segment names are inconsistent today, so the match is the real problem. Module 7 pulls the lists index; module 8 keeps the list whose parsed date range contains the booked session's date and whose tokens match the course level and delivery. For a session on 2026-07-13 at HS level, that is the list named "7/6-7/17/2026 HS Class" (its range contains 7/13 and it carries the HS token); a list ending 6/13 is out, and a list that contains 7/13 but reads MS is out on the level token. Zero or more than one match takes the flag path.

5. Flag, do not fail

The scenario never guesses a segment. When the resolver returns zero or more than one candidate list, module 13 emails the admin with the student, course, session date, and the match count, and module 14 stops the scenario. No records are changed until a human resolves it. The same posture governs the zero-booking path: the scenario asks the parent for dates and stops rather than inventing an enrollment. A person deciding beats a wrong write, and every stop is explicit rather than a silent failure.

6. Appiant coexistence

Your existing Appiant integration keeps MindBody and HubSpot contact records in sync. This scenario must not fight it. Two rules enforce that:

The result is a clean separation. Appiant owns identity. This scenario owns the enrollment state, and the two never write the same field.

7. What the demo simplifies (production notes)

This build is deliberately a single importable scenario. Two choices are worth calling out, with the higher-volume alternative for each, plus what the real MindBody sandbox says about production routing.

The delay: Sleep now, data store plus scheduler later

The ten-minute wait uses two chained Sleep modules, which keeps everything in one scenario and easy to import. For higher volume the recommended pattern is a Make data store plus a scheduled second scenario: the webhook writes a pending row carrying a process-after time, and a scheduler picks it up once the wait has passed. That frees the operation slot during the wait and scales more cleanly.

The segment resolver: hosted, or fully in-Make

Module 8 points at a small resolver in code because parsing inconsistent list names (different word order, optional course tokens, mixed date formats) is fragile inside Make formulas. In production you point it at your hosted resolver. If you want no external code at all, a fully self-contained in-Make alternative works: pull the lists index, run an Iterator over the lists, apply a per-list containment filter (parse each list's date range and keep it only if it contains the session date and the level and delivery tokens match), and collect the survivors with an Array aggregator. Exactly one survivor proceeds; zero or more than one takes the flag path.

Real MindBody findings that shape production

8. Error handling and edge cases

SituationHandling
Signature does not matchReject the bundle before any downstream module runs. A mismatch means the payload is not from MindBody.
Sale is not a course purchaseFilter the proceed route on the item type or course id so unrelated sales do not run enrollment.
clientSale.created has no emailResolve the email first with GET /client/clients on the clientId before the HubSpot steps. Verified against the real sandbox: the sale event carries the client id reliably; email is not guaranteed.
Nothing booked after the waitEmail the parent to ask for their dates, then stop. Staff finish the booking by hand and the scenario does not re-trigger. Normal for the single-session AI Workshop.
Parent books several sessions of one courseThe dedup read of the target segment (module 15) stops all but the first booking, so nothing is written twice.
Segment match is 0 or more than 1Email the admin and stop. The scenario never guesses a segment. Nothing is changed until a person resolves it.
HubSpot list add returns a 403 scopes errorThe connection needs the CRM lists scopes. In Make, open the connection's Edit then Custom Scopes, add crm.lists.read and crm.lists.write, save, and reauthorize (a contacts-only connection reads and writes contacts fine but 403s on the list step, which halts the run).
Same-second double booking the membership read could missOptional belt-and-suspenders: a Make data-store key of student plus course plus week closes the narrow window the membership read alone could miss.
Transient API failureSet the scenario's error handling to retry the HTTP and HubSpot modules. Route unrecoverable errors to a data store or a notification so nothing fails silently.

9. Importing and what to supply

  1. In Make, create a new scenario and import coursesync.blueprint.json.
  2. Attach connections: MindBody credentials on the HTTP modules, HubSpot (with the crm.lists scopes), Google Sheets, and the send connection.
  3. Supply the values: SiteId, the HubSpot confirmation template id, the spreadsheet id, the admin recipient, the VA recipient, and point module 8 at your resolver (or replace it with the in-Make alternative in section 7).
  4. Configure the custom webhook and the MindBody clientSale.created subscription, then store the signature key.
  5. Optional: once the StartIntegrate connector is installed in the account, swap the MindBody HTTP calls for the native actions where one exists. The visits read stays a Make an API Call because the connector has no get-visits action.
  6. Run once with a test sale and confirm the path end to end: the override read, the wait, the booking check, the dedup read, exactly one segment matched, the course appended to courses-taken, the list membership added, the audit row appended with MB Enrollment, the VA alerted for the summer in-person Study Skills case, and the draft confirmation queued for review.