Build documentation · sample runbook
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.
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.
| Connection | Used by | Notes |
|---|---|---|
| MindBody API | Trigger, module 6 | Read 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. |
| HubSpot | Modules 2, 7, 15, 16, 17, 20 | Private 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 Sheets | Module 18 | Access to the target spreadsheet with an Enrollments tab that includes the MB Enrollment column. |
| Gmail or email | Modules 11, 13, 19 | Any 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 resolver | Module 8 | A 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).
The StartIntegrate connector has no purchase trigger, so the scenario is fired by a MindBody webhook, not a connector poll.
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.
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.
Module numbers match the reference table below. The gates named on each route are the filters that select it.
| ID | Module | Purpose | Key mappings |
|---|---|---|---|
| 1 | gateway:CustomWebHook v1 | Receive clientSale.created from MindBody. | webhook + signature configured after import |
| 2 | hubspotcrm: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 |
| 3 | util:SetVariables v1 | Context vars. | override, contactId, contactEmail, coursesTaken, courseName, delivery |
| 4 | util:FunctionSleep v1 | STEP 1 wait, part 1 of 2. | duration 300s (1s when override is set) |
| 5 | util:FunctionSleep v1 | STEP 1 wait, part 2 of 2 (Sleep maxes at 300s). | duration 300s (1s when override is set) |
| 6 | http: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}} |
| 7 | hubspotcrm:MakeAPICall v2 | Pull the HubSpot lists index for segment matching. | POST /crm/v3/lists/search; MANUAL and SNAPSHOT lists; each with listId + name |
| 8 | http:ActionSendData v3 | Segment resolver: date-range containment match. | POST session date + course tokens + lists index; returns matchCount, matchedListId, matchedListName |
| 9 | util:SetVariables v1 | Decision vars. | bookingCount, matchCount, matchedListId, matchedListName, enrollmentMode, vaEligible |
| 10 | builtin:BasicRouter v1 | Route to zero-booking, flag, or proceed. | three routes, selected by filters |
| 11 | google-email:sendAnEmail v4 | Zero booking: email the parent for dates. | filter bookingCount = 0 AND override not true |
| 12 | builtin:StopScenario v1 | Stop (manual completion). | no re-trigger; steps 2-6 do not run |
| 13 | google-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) |
| 14 | builtin:StopScenario v1 | Stop (needs a human). | nothing is written until a person resolves the segment |
| 15 | hubspotcrm:MakeAPICall v2 | Dedup: read the target segment's memberships. | GET /crm/v3/lists/{{matchedListId}}/memberships; filter matchCount = 1 |
| 16 | hubspotcrm:upsertAContact v2 | STEP 2: append the course to the multi-select. | filter not already a member; coursesync_ properties only; courses_taken appended, not overwritten |
| 17 | hubspotcrm:addMembersToAList v2 | STEP 3: add the contact to the matched segment (native). | listId {{matchedListId}}; recordIds ["{{16.id}}"]; needs crm.lists r/w |
| 18 | google-sheets:addRow v2 | STEP 4: append an audit row with the MB Enrollment column. | Enrollments tab; Timestamp, Student, Course, Segment, MB Enrollment, SaleId |
| 19 | google-email:sendAnEmail v4 | STEP 5: VA alert, conditional. | filter vaEligible = true (MS or HS SSS, in-person, summer) |
| 20 | hubspotcrm:MakeAPICall v2 | STEP 6: draft the confirmation to the admin. | POST /marketing/v4/email/single-send; subject prefixed DRAFT COURSE CONF |
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.
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 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.
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.
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.
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 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.
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.
| Situation | Handling |
|---|---|
| Signature does not match | Reject the bundle before any downstream module runs. A mismatch means the payload is not from MindBody. |
| Sale is not a course purchase | Filter the proceed route on the item type or course id so unrelated sales do not run enrollment. |
| clientSale.created has no email | Resolve 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 wait | Email 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 course | The 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 1 | Email 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 error | The 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 miss | Optional 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 failure | Set 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. |