Project · iD Tech · Iterable · Custom events

Quiz Event to Recommendation Email

The plumbing behind the Course Recommendation Quiz: the profile update that keeps a parent's answers, the custom event that triggers the email, and the Handlebars that turns six recommendations into it. One parent, Priya, takes the quiz for her 10-year-old, Maya. Course names, IDs and URLs are examples.

  • Iterable
  • Custom events
  • REST API
  • Handlebars
  • Catalog
  • Personalization
Quiz submitted→users/update→events/track · Quiz→Journey→Recommendation email

When a parent finishes the quiz, the web server makes two calls. The profile update saves the answers where segments and every later email can reach them. The event starts the Journey and carries the content for this one email.

Profile first, event second. Even if the Journey starts before the profile write lands, the results email still renders, because it reads the event. The profile only has to be in place for the emails that follow, days later.

Email is the identifier. A quiz lead arrives with an email address and nothing else, which suits an email-based or hybrid project. In a userID-based project, like the one on the API page, the web server would have to create a userId first.

Saving the answers

The answers go into a single nested object, lastQuiz. Segments can then target a pathway or a campus preference, and later emails can open with "Still thinking about Robotics for Maya?" months after the quiz.

POST/api/users/update
{
  "email": "priya.k@example.com",
  "dataFields": {
    "firstName": "Priya",
    "lastQuiz": {
      "studentName": "Maya",
      "studentAge": 10,
      "ageBracket": "9-12",
      "pathways": ["Coding", "Robotics"],
      "location": "both",
      "submittedAt": "2026-02-03 18:22:00 +00:00"
    }
  }
}

mergeNestedObjects is left off on purpose. The API page turns it on so one loyalty field doesn't wipe the rest. Here the opposite is right: a new quiz should replace lastQuiz whole, so a pathway Priya picked last time can't linger next to this year's answers.

One student per quiz, so the latest answers win. Every quiz is still kept as a Quiz event, so a family that quizzes for two children has both in its event history even though lastQuiz holds one.

The Quiz event

The event carries everything the results email shows: the six courses, already chosen and split by location, with their names, images and links. The web server runs the recommendation logic; Iterable only renders it.

POST/api/events/track
{
  "email": "priya.k@example.com",
  "eventName": "Quiz",
  "dataFields": {
    "studentName": "Maya",
    "studentAge": 10,
    "ageBracket": "9-12",
    "pathways": ["Coding", "Robotics"],
    "location": "both",
    "campusCourses": [
      {
        "id": "COD-101-C",
        "name": "Intro to Python",
        "pathway": "Coding",
        "imageUrl": "https://cdn.example.com/courses/cod-101.jpg",
        "url": "https://www.example.com/courses/cod-101?format=campus"
      },
      {
        "id": "COD-102-C",
        "name": "Code Your First App",
        "pathway": "Coding",
        "imageUrl": "https://cdn.example.com/courses/cod-102.jpg",
        "url": "https://www.example.com/courses/cod-102?format=campus"
      },
      {
        "id": "ROB-101-C",
        "name": "Build & Code a Rover",
        "pathway": "Robotics",
        "imageUrl": "https://cdn.example.com/courses/rob-101.jpg",
        "url": "https://www.example.com/courses/rob-101?format=campus"
      }
    ],
    "onlineCourses": [
      {
        "id": "COD-103-O",
        "name": "JavaScript Game Logic",
        "pathway": "Coding",
        "imageUrl": "https://cdn.example.com/courses/cod-103.jpg",
        "url": "https://www.example.com/courses/cod-103?format=online"
      },
      {
        "id": "ROB-102-O",
        "name": "Sensors & Autonomy",
        "pathway": "Robotics",
        "imageUrl": "https://cdn.example.com/courses/rob-102.jpg",
        "url": "https://www.example.com/courses/rob-102?format=online"
      },
      {
        "id": "ROB-103-O",
        "name": "Circuits & Microcontrollers",
        "pathway": "Robotics",
        "imageUrl": "https://cdn.example.com/courses/rob-103.jpg",
        "url": "https://www.example.com/courses/rob-103?format=online"
      }
    ]
  }
}

Leave out what isn't there. A parent who picks online only gets no campusCourses field at all, rather than an empty array. The template tests for the field with {{#if}}, and a missing field is unambiguous.

Event history does the targeting. The "most likely to enroll" ranking on the quiz page is a segment: bought last season and has a Quiz event since. That only works because every submission is kept as an event, not just the latest answers.

The Journey is triggered by the Quiz event, so the template reads the event's fields by name: studentName, pathways and the two course arrays. For a results email that's the right source. It shows exactly what Maya's answers produced at the moment she finished.

Campus first is a template decision, not a sort order. The campus block is written above the online block, so campus leads whatever order the courses arrive in. Campus is the preferred purchase.

join prints the pathway list as "Coding + Robotics" without a loop, and each {{#each}} repeats one table row per course, so three courses or six render from the same markup.

Handlebars · email template
<h1>{{studentName}}'s top 6 courses</h1>
<p>Matched to {{join pathways " + "}}, ages {{ageBracket}}.</p>

{{#if campusCourses}}
<h2>On campus</h2>
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
{{#each campusCourses}}
  <tr>
    <td><img src="{{imageUrl}}" alt="{{name}}" width="96"></td>
    <td><a href="{{url}}">{{name}}</a><br>{{pathway}}</td>
  </tr>
{{/each}}
</table>
{{/if}}

{{#if onlineCourses}}
<h2>Live online</h2>
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
{{#each onlineCourses}}
  <tr>
    <td><img src="{{imageUrl}}" alt="{{name}}" width="96"></td>
    <td><a href="{{url}}">{{name}}</a><br>{{pathway}}</td>
  </tr>
{{/each}}
</table>
{{/if}}

Pick a location and the pathways Maya's parent chose. The email and the events/track payload rebuild together, using the same six-slot split as the quiz page.

Location
Pathways · most popular first
Maya's top 6 courses · iD Techto priya.k@example.com
POSTevents/track · these answers

The original event carried the course copy, photos and links on every submission. If I were building it today, the courses would live in an Iterable Catalog and the event would carry only their IDs. The template looks each one up when the email renders.

Catalog · Courses · item COD-101-C
{
  "name": "Intro to Python",
  "pathway": "Coding",
  "format": "campus",
  "ages": "9-12",
  "imageUrl": "https://cdn.example.com/courses/cod-101.jpg",
  "url": "https://www.example.com/courses/cod-101?format=campus",
  "seatsLeft": 6
}
POSTevents/track · IDs only
{
  "email": "priya.k@example.com",
  "eventName": "Quiz",
  "dataFields": {
    "studentName": "Maya",
    "studentAge": 10,
    "ageBracket": "9-12",
    "pathways": ["Coding", "Robotics"],
    "location": "both",
    "campusCourseIds": ["COD-101-C", "COD-102-C", "ROB-101-C"],
    "onlineCourseIds": ["COD-103-O", "ROB-102-O", "ROB-103-O"]
  }
}

#catalogKeys takes the catalog name and an array field of item keys, and hands back the matching items to loop over. The event field campusCourseIds works as that array.

Because the lookup happens at render time, details that change, like seats left, are current when the email sends rather than frozen when the quiz was taken.

Handlebars · Catalog version
{{#if campusCourseIds}}
<h2>On campus</h2>
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
{{#catalogKeys "Courses" campusCourseIds as |courses|}}
  {{#each courses}}
  <tr>
    <td><img src="{{this.imageUrl}}" alt="{{this.name}}" width="96"></td>
    <td><a href="{{this.url}}">{{this.name}}</a><br>
        {{this.pathway}} · {{this.seatsLeft}} seats left</td>
  </tr>
  {{/each}}
{{/catalogKeys}}
</table>
{{/if}}

{{!-- Live online: the same block, reading onlineCourseIds --}}
Content in the eventIDs plus a Catalog
Course copy and imagesSent by the web server on every submissionStored once, in the Courses catalog
Renaming a courseA change to the web server's codeAn edit to one catalog item
Seats left, datesFrozen at quiz timeCurrent at send time
Event sizeSix full course objectsSix IDs
Old events in segmentsHold copy that may be out of dateHold stable IDs
Back to Projects

© 2026 Cheyne Mosher. All rights reserved.

Back arrow to go back to main home page.