Skip to main content
This page covers all backend functions for courses and shows how to use them in .ef page templates. See Courses — Overview for setup and enrollment.

Backend functions

All functions are available in <script scope="backend"> blocks and in .ef files.

getCourses(assignedOnly?)

Returns all courses for the brand, each with is_assigned and started for the current customer.
ParameterTypeDefaultDescription
assignedOnlybooleanfalsefalse — all courses (use is_assigned to show “Get access” vs content). true — only courses the customer is enrolled in.
var allCourses    = getCourses(false);   // all, with is_assigned per course
var myCourses     = getCourses(true);    // enrolled only

getCategoriesWithCourses(assignedOnly?)

Same as getCourses but grouped by category. Returns an array of { category_slug, category_name, courses[] }.
var categories = getCategoriesWithCourses(false);
// categories[0].category_name  → "Daily Practices"
// categories[0].courses        → [ { title, slug, is_assigned, ... } ]

getCoursesByCategorySlug(categorySlug, assignedOnly?)

Returns courses in a single category. Useful for “Daily practices” or “Advanced” sections on a dashboard.
var daily = getCoursesByCategorySlug('daily-practices', false);

getCoursesForCustomer()

Returns only courses the current customer is enrolled in (same as getCourses(true)). Returns [] if no session or no enrollments.

getCourseForCustomer(slug)

Returns a single course with enrollment check — is_assigned and started are set for the current customer. Also returns the full modules[] and contents[] tree, plus progress_percent and completed_content_ids. Use this on the course detail page.
var course = getCourseForCustomer('tinnitus-recovery');
// course.is_assigned  → true / false
// course.modules      → full module/lesson tree
// course.progress_percent → 0–100
Returns null if no course with that slug exists.

getCourseBySlug(slug)

Returns a single course without enrollment check. is_assigned and started will not be set. Use when you only need course metadata and don’t need to gate content.
var course = getCourseBySlug('tinnitus-recovery');

request.route_params

For dynamic slug pages (e.g. guided-course/{course}.ef), the slug is available via request.route_params.course.

Course list page

Show all courses, sorted so enrolled courses appear first. Use is_assigned and started to drive the CTA label.
<script scope="backend">
  if (!is_customer) {
    redirect('/members-login');
  }

  var allCourses = getCourses(false);

  // Sort: enrolled first, then among enrolled put started (Continue) before not started
  allCourses = allCourses.slice().sort(function(a, b) {
    if (a.is_assigned !== b.is_assigned) return b.is_assigned - a.is_assigned;
    return b.started - a.started;
  });

  setVariable('courses', allCourses);
  setVariable('courseCount', allCourses.length);
</script>

@if(var.courseCount gt 0)
<div class="course-grid">
  @foreach(c in var.courses)
  <div class="course-card">
    @if(c.image_url)
    <img src="{{ c.image_url }}" alt="{{ c.title }}" />
    @endif

    <h3>{{ c.title }}</h3>
    <p>{{ c.description }}</p>
    <p>{{ c.module_count }} modules · {{ c.lesson_count }} lessons</p>

    @if(c.is_assigned eq false)
    <a href="/buy/{{ c.slug }}-access" class="btn-secondary">Get access</a>
    @elseif(c.started)
    <a href="/course/{{ c.slug }}" class="btn-primary">Continue</a>
    @else
    <a href="/course/{{ c.slug }}" class="btn-primary">Start Course</a>
    @endif
  </div>
  @endforeach
</div>
@else
<p>No courses available yet.</p>
@endif

With category tabs

<script scope="backend">
  if (!is_customer) { redirect('/members-login'); }

  var categories = getCategoriesWithCourses(false);

  // Sort courses within each category: assigned first
  categories = categories.map(function(cat) {
    var sorted = (cat.courses || []).slice().sort(function(a, b) {
      if (a.is_assigned !== b.is_assigned) return b.is_assigned - a.is_assigned;
      return b.started - a.started;
    });
    return { category_slug: cat.category_slug, category_name: cat.category_name, courses: sorted };
  });

  setVariable('categories', categories);
</script>

@foreach(cat in var.categories)
<section data-category="{{ cat.category_slug }}">
  <h2>{{ cat.category_name }}</h2>
  <div class="course-grid">
    @foreach(c in cat.courses)
    <div class="course-card">
      <h3><a href="/course/{{ c.slug }}">{{ c.title }}</a></h3>
      <span>
        @if(c.is_assigned eq false)
          Get access
        @elseif(c.started)
          Continue
        @else
          Start Course
        @endif
      </span>
    </div>
    @endforeach
  </div>
</section>
@endforeach

Course detail page

Resolve the slug from the URL, check enrollment with getCourseForCustomer, and gate content.
<script scope="backend">
  if (!is_customer) {
    redirect('/members-login');
  }

  // For a dynamic page named guided-course/{course}.ef:
  var slug = request.route_params.course;

  // For a fixed page reading slug from query string:
  // var slug = request.query.course;

  var course = getCourseForCustomer(slug);

  if (!course) {
    redirect('/courses');
  }

  setVariable('course',       course);
  setVariable('isAssigned',   course.is_assigned || false);
  setVariable('courseStarted', course.started || false);

  if (course.is_assigned) {
    // Track that the customer has visited this course (optional)
    setSessionItem('course_visited_' + slug, '1');
  }
</script>

@if(var.course)

  <!-- Hero -->
  <div class="course-hero">
    @if(var.course.cover)
    <img src="{{ var.course.cover }}" alt="{{ var.course.title }}" />
    @endif
    <h1>{{ var.course.title }}</h1>
    @if(var.course.instructor_name)
    <p>by {{ var.course.instructor_name }}</p>
    @endif
  </div>

  <!-- Stats -->
  <div class="course-stats">
    <span>{{ var.course.module_count }} modules</span>
    <span>{{ var.course.total_lesson_count }} lessons</span>
  </div>

  @if(var.isAssigned)

  <!-- Progress -->
  <div class="progress-bar" style="width: {{ var.course.progress_percent }}%"></div>
  <p>{{ var.course.progress_percent }}% complete</p>

  <!-- Modules and lessons -->
  @foreach(mod in var.course.modules)
  <div class="module" id="module-{{ mod.id }}">
    <h2>{{ mod.order }}. {{ mod.title }}</h2>
    <div class="lessons">
      @foreach(lesson in mod.contents)
      <div class="lesson-row"
           data-content-id="{{ lesson.id }}"
           data-content-url="{{ lesson.content_url }}">
        <span class="lesson-title">{{ lesson.title }}</span>
        @if(lesson.type == 'video')
        <span class="badge">Video</span>
        @elseif(lesson.type == 'audio')
        <span class="badge">Audio</span>
        @endif
      </div>
      @endforeach
    </div>
  </div>
  @endforeach

  @else

  <!-- Not enrolled — show CTA -->
  <div class="access-gate">
    <p>You don't have access to this course yet.</p>
    <a href="/courses" class="btn">Browse all courses</a>
  </div>

  @endif

@else
<p>Course not found. <a href="/courses">Back to courses</a></p>
@endif

Auth wrapper for course pages

Use the auth wrapper pattern (@extends) so the login check and common data load happen once across all course pages:
{{-- members-courses-layout.ef --}}
<script scope="backend">
  if (!is_customer) {
    redirect('/members-login?next=' + encodeURIComponent(request.path));
  }
</script>
@block("content") @endblock
Then each course page:
{{-- course-list.ef --}}
@extends("members-courses-layout")
@block("content")

<script scope="backend">
  var courses = getCourses(false);
  setVariable('courses', courses);
</script>

@foreach(c in var.courses)
  <p>{{ c.title }} — @if(c.is_assigned)Enrolled @else Get access @endif</p>
@endforeach

@endblock

Checking access to a specific product

If you want to gate course access by checking whether the customer bought a specific product (rather than relying on enrollment), you can cross-reference with getOrders():
<script scope="backend">
  if (!is_customer) { redirect('/members-login'); }

  var course = getCourseForCustomer(request.route_params.course);
  if (!course) { redirect('/courses'); }

  // Fallback: check order history for a product by name
  if (!course.is_assigned) {
    var orders = getOrders('newest', 100);
    for (var i = 0; i < orders.length; i++) {
      for (var j = 0; j < orders[i].products.length; j++) {
        if (orders[i].products[j].name === 'Course Access Pass') {
          course.is_assigned = true;
        }
      }
    }
  }

  setVariable('course', course);
  setVariable('isAssigned', course.is_assigned);
</script>
Prefer the enrollment-based check (getCourseForCustomer) — it is more reliable and does not require iterating order history. Use the order-history fallback only if enrollment is not being set up via product linking.

Lesson types and content

Each lesson in mod.contents has a type field:
typeContent fields
'video'content_url (direct video URL) or content_embed (iframe HTML)
'audio'content_url (audio file URL)
'text'content (HTML body)
Additional fields on each lesson:
FieldDescription
titleLesson title
descriptionShort description shown below the player
content_underAdditional text shown under video/audio (e.g. transcript excerpt)
materialsArray of { name, url } — downloadable files
key_takeawaysArray of strings — bullet-point summary

Progress API

Mark a lesson complete from the browser:
fetch('/api/course/progress', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    course_id: courseId,
    content_id: lessonId,
    completed: true
  })
});
The page receives the initial completed IDs via course.completed_content_ids. Use this array to mark lessons visually on load, then update the UI optimistically when the API call succeeds.