How to Create a Course

A complete guide to building an adaptive learning course on Graspful. Every section explains the concept first, then shows a worked example using a JavaScript Fundamentals course as illustration. The principles apply to any subject — firefighting, real estate, AWS, or anything else that can be broken into learnable concepts.

1. Choose Your Subject

Graspful works best for skill-based subjects that can be broken into discrete, testable concepts. Good candidates have a natural prerequisite structure — some topics must be understood before others make sense.

What makes a good Graspful course

  • Skill-based: Students need to apply knowledge, not just recall it. Certification exams, professional skills, and technical subjects are ideal.
  • Decomposable: The subject can be split into 20-200 concepts, each independently testable. If you can write 3+ practice problems for an idea, it is probably a concept.
  • Prerequisite-rich: Some topics depend on others. "Closures" requires "functions" which requires "variables." This structure is what makes adaptive learning powerful — without it, you just have a quiz bank.

Single course vs. academy

Use a single course when the subject has one clear learning path (e.g., "JavaScript Fundamentals"). Use an academy when you have multiple related courses that share foundational concepts (e.g., a "Web Development Academy" containing JavaScript, TypeScript, and React courses). Academies let prerequisites span course boundaries.

2. Design the Knowledge Graph

The knowledge graph is the skeleton of your course. Get this right and everything else follows. Get it wrong and no amount of great content will save the learning experience.

Start with the tree, not the prose

Do not start by writing lessons. Start by listing every concept in your subject and drawing the prerequisite arrows between them. Think of it as a dependency graph: what does a student need to know before they can understand this idea? Write the graph structure first, then fill in the content.

Identify atomic concepts

A concept is one teachable idea that can be tested independently. The test: can you write 3 distinct practice problems for this concept that do not require knowledge from any other concept? If yes, it is a good concept. If you need to explain two unrelated things to write the problems, the concept is too broad — split it. If you cannot write 3 distinct problems, it is too narrow — merge it into a parent concept as a knowledge point.

Map prerequisites

For each concept, ask: "What must the student already know?" List only direct prerequisites — max 3-4 per concept. Transitive prerequisites are inferred automatically. If A requires B and B requires C, do not list C as a prerequisite of A. The prerequisite graph must be a DAG (no cycles).

Add encompassing edges

Encompassing edges answer: "When a student practices this advanced concept, which foundational concepts get exercised?" For example, every closure problem exercises the student's understanding of functions and variables. Adding encompassing edges with appropriate weights (0.2-1.0) lets the spaced repetition engine grant implicit review credit, reducing the total review burden.

Example: JavaScript course graph skeleton

This is the structure only — no content yet. Notice how the prerequisite arrows form a DAG, and encompassing edges capture implicit practice relationships.

js-fundamentals.yaml (graph skeleton)
course:
  id: js-fundamentals
  name: JavaScript Fundamentals
  description: Core JavaScript for web developers
  estimatedHours: 25
  version: "2026.1"
  sourceDocument: "MDN Web Docs"

sections:
  - id: basics
    name: Language Basics
    description: Variables, types, and operators
  - id: functions
    name: Functions
    description: Declarations, scope, and closures
  - id: async
    name: Asynchronous JavaScript
    description: Callbacks, promises, and the event loop

concepts:
  # --- Basics ---
  - id: variables
    name: Variables and Declarations
    section: basics
    difficulty: 2
    estimatedMinutes: 15
    prerequisites: []
    knowledgePoints: []   # stub — fill later

  - id: data-types
    name: Primitive Data Types
    section: basics
    difficulty: 2
    estimatedMinutes: 20
    prerequisites: [variables]
    knowledgePoints: []

  - id: operators
    name: Operators and Expressions
    section: basics
    difficulty: 3
    estimatedMinutes: 20
    prerequisites: [variables, data-types]
    knowledgePoints: []

  # --- Functions ---
  - id: function-declarations
    name: Function Declarations and Expressions
    section: functions
    difficulty: 3
    estimatedMinutes: 25
    prerequisites: [variables, operators]
    knowledgePoints: []

  - id: scope
    name: Scope and Hoisting
    section: functions
    difficulty: 4
    estimatedMinutes: 25
    prerequisites: [function-declarations]
    encompassing:
      - concept: variables
        weight: 0.5
    knowledgePoints: []

  - id: closures
    name: Closures
    section: functions
    difficulty: 6
    estimatedMinutes: 30
    prerequisites: [scope]
    encompassing:
      - concept: function-declarations
        weight: 0.7
      - concept: scope
        weight: 0.8
      - concept: variables
        weight: 0.4
    knowledgePoints: []

  # --- Async ---
  - id: callbacks
    name: Callbacks
    section: async
    difficulty: 5
    estimatedMinutes: 20
    prerequisites: [function-declarations]
    encompassing:
      - concept: function-declarations
        weight: 0.6
    knowledgePoints: []

  - id: promises
    name: Promises
    section: async
    difficulty: 6
    estimatedMinutes: 30
    prerequisites: [callbacks]
    encompassing:
      - concept: callbacks
        weight: 0.5
    knowledgePoints: []

  - id: event-loop
    name: The Event Loop
    section: async
    difficulty: 7
    estimatedMinutes: 35
    prerequisites: [promises, closures]
    encompassing:
      - concept: promises
        weight: 0.6
      - concept: callbacks
        weight: 0.4
      - concept: closures
        weight: 0.3
    knowledgePoints: []

3. Author Knowledge Points

Knowledge points (KPs) are the progressive steps within each concept. They form the learning staircase — each step builds on the last, and students climb one step at a time.

The staircase: recognition, guided application, transfer

  • KP1 — Recognition: Can the student identify the concept? Define terms, recognize examples, distinguish it from similar ideas.
  • KP2 — Guided application: Can the student use it with support? Apply a formula, follow a procedure, work a standard problem.
  • KP3 — Transfer: Can the student apply it to a novel scenario? Debug unfamiliar code, choose the right approach for a new situation, combine with other concepts.

Not every concept needs exactly 3 KPs. Simple concepts (difficulty 1-3) might have 2. Complex concepts (difficulty 7+) might have 4. The key is that each KP teaches one distinct thing and the difficulty increases monotonically.

Writing instruction text

Instruction text is what the student sees before practicing. Write it for audio — Graspful generates TTS from instruction text. This means: short sentences, no walls of text, no markdown tables (they do not work in audio). If instruction exceeds 100 words, you must add structured instructionContent blocks (images, callouts) to break it up. The review gate enforces this.

Writing worked examples

A worked example shows the concept applied step by step. It bridges instruction and practice — the student sees how to think through the problem before attempting one themselves. At least 50% of authored concepts should have at least one KP with a worked example. Focus worked examples on the higher-difficulty KPs where students are most likely to get stuck.

Example: closures concept with 3 KPs

closures concept — knowledge points
  - id: closures
    name: Closures
    section: functions
    difficulty: 6
    estimatedMinutes: 30
    prerequisites: [scope]
    encompassing:
      - concept: function-declarations
        weight: 0.7
      - concept: scope
        weight: 0.8
      - concept: variables
        weight: 0.4
    knowledgePoints:
      - id: closures-kp1
        instruction: |
          A closure is a function that remembers the variables from the
          scope where it was created, even after that scope has closed.
          Every function in JavaScript is a closure. When you write a
          function inside another function, the inner function can access
          the outer function's variables — even after the outer function
          has returned.
        problems: []  # filled in step 4

      - id: closures-kp2
        instruction: |
          Closures are commonly used to create private state. You write a
          function that declares a variable, then returns an inner function
          that reads or modifies that variable. The variable is invisible
          to the outside world — only the returned function can access it.
        workedExample: |
          Problem: Create a counter that starts at 0 and increments by 1
          each time it is called.

          Step 1: Write an outer function that declares a count variable.
            function makeCounter() { let count = 0; }

          Step 2: Return an inner function that increments and returns count.
            function makeCounter() {
              let count = 0;
              return function() { count += 1; return count; };
            }

          Step 3: Call the outer function to get a counter.
            const counter = makeCounter();
            counter(); // 1
            counter(); // 2

          The inner function closes over count. Each call to makeCounter
          creates a new, independent count variable.
        problems: []

      - id: closures-kp3
        instruction: |
          A common closure bug happens in loops. When you create functions
          inside a loop using var, all functions share the same variable
          and see its final value. The fix: use let (which creates a new
          binding per iteration) or wrap the body in an IIFE.
        workedExample: |
          Bug: This code logs "3" three times instead of "0, 1, 2":
            for (var i = 0; i < 3; i++) {
              setTimeout(function() { console.log(i); }, 100);
            }
          Why: All three functions close over the same i, which is 3
          when the timeouts fire.

          Fix: Change var to let:
            for (let i = 0; i < 3; i++) {
              setTimeout(function() { console.log(i); }, 100);
            }
          Now each iteration creates a new i binding, so each function
          closes over its own copy.
        problems: []

4. Write Problems

Problems are how the engine tests and tracks mastery. The quality of your problems directly determines the quality of the adaptive experience.

Problem types

multiple_choice

4 options, 1 correct. The workhorse. Use for most problems.

true_false

Binary choice. Good for testing precise definitions and common misconceptions.

fill_blank

Free-text answer. The system normalizes input. Good for recall.

ordering

Arrange 4-6 steps in sequence. Good for procedures and algorithms.

matching

Match items from two columns. Good for terminology and associations.

scenario

Rich context followed by a question. Best for complex application.

The deduplication rule

No two problems should test the same fact at the same cognitive level. The review gate checks for near-duplicate questions via normalized text hashing. To create distinct variants, change the scenario, vary the distractors, adjust the difficulty, or test the concept from a different angle.

Variant depth: 3-4 minimum per KP

Every knowledge point needs at least 3 problems. This gives the adaptive engine enough material to (a) verify mastery with two consecutive correct answers and (b) retry with a different problem if the student fails. Four or more is better. The review gate blocks publishing if any KP has fewer than 3.

Writing explanations that diagnose misconceptions

An explanation should not just reveal the answer. It should name the likely misconception that led to the wrong answer. Instead of "The answer is B," write "If you chose A, you may be confusing closures with callbacks. The key difference is..." Good explanations turn wrong answers into learning moments.

Example: closures problems at three difficulty levels

closures-kp1 problems
        problems:
          # Difficulty 1 — recognition
          - id: closures-kp1-p1
            type: multiple_choice
            question: "What is a closure in JavaScript?"
            options:
              - "A function that remembers variables from its creation scope"
              - "A function that runs immediately when defined"
              - "A function that cannot access outer variables"
              - "A function that is only callable once"
            correct: 0
            explanation: >
              A closure is a function bundled with references to its surrounding
              scope. If you chose B, you may be thinking of an IIFE (Immediately
              Invoked Function Expression), which is a separate concept.
            difficulty: 1

          # Difficulty 2 — recognition with nuance
          - id: closures-kp1-p2
            type: true_false
            question: >
              True or false: Every function in JavaScript is technically a closure.
            options: ["True", "False"]
            correct: 0
            explanation: >
              True. In JavaScript, every function closes over the scope in which
              it was defined. We typically use the term "closure" for cases where
              this behavior is deliberate and observable — but technically, all
              functions are closures.
            difficulty: 2

          # Difficulty 3 — identify a closure in code
          - id: closures-kp1-p3
            type: multiple_choice
            question: >
              Which line creates a closure?
              function greet(name) {
                return function() { console.log("Hi " + name); };
              }
            options:
              - "Line 1: function greet(name)"
              - "Line 2: return function()"
              - "Line 2: the returned function closes over 'name'"
              - "No closure is created in this code"
            correct: 2
            explanation: >
              The anonymous function returned on line 2 closes over the 'name'
              parameter from greet's scope. When greet finishes executing, the
              returned function still has access to 'name'. If you chose A, note
              that greet itself does not close over anything unusual — it is the
              inner function that forms the closure.
            difficulty: 3

          # Difficulty 4 — predict output
          - id: closures-kp1-p4
            type: multiple_choice
            question: >
              What does this code output?
              function outer() {
                let x = 10;
                function inner() { console.log(x); }
                x = 20;
                return inner;
              }
              outer()();
            options:
              - "10"
              - "20"
              - "undefined"
              - "ReferenceError"
            correct: 1
            explanation: >
              The inner function closes over the variable x, not its value at
              the time of creation. When inner executes, x has been reassigned
              to 20. If you chose A, remember: closures capture variables by
              reference, not by value.
            difficulty: 4

5. Add Section Exams

Section exams are optional cumulative assessments at the end of a section. They gate progression: a student must pass the exam before moving to concepts in the next section. Use section exams when your course has natural groupings where you want to verify broad understanding before advancing.

When to use sections vs. flat concept lists

Use sections when your course has 15+ concepts and natural groupings. A flat concept list (no sections) works fine for smaller courses where the prerequisite graph alone provides enough structure. You do not need to add a section exam to every section — only add them where cumulative verification adds value.

Blueprint design

The exam blueprint specifies minimum questions per concept, ensuring broad coverage. The total questionCount must be greater than or equal to the sum of all minQuestions in the blueprint. The engine fills remaining slots by sampling from all section concepts. Design for unaided reasoning — no hints, no instruction text.

Passing score and time limits

The default passing score is 75%. You can adjust it per section. Time limits are optional — if set, the exam auto-submits when time runs out. A good heuristic: allow 1-2 minutes per question.

Example: Functions section exam

section exam blueprint
sections:
  - id: functions
    name: Functions
    description: Declarations, scope, and closures
    sectionExam:
      enabled: true
      passingScore: 0.75
      timeLimitMinutes: 20
      questionCount: 12
      blueprint:
        - conceptId: function-declarations
          minQuestions: 3
        - conceptId: scope
          minQuestions: 3
        - conceptId: closures
          minQuestions: 4
      instructions: >
        This exam covers all concepts in the Functions section.
        You have 20 minutes to complete 12 questions. No notes
        or references are allowed. You need 75% to pass.

6. Set Difficulty and Metadata

Each concept has metadata that calibrates the adaptive engine and helps with course organization.

Difficulty scale (1-10)

Difficulty is a concept-level rating that tells the engine how hard this idea is relative to other concepts in the course. The scale:

LevelDescriptionExample
1-2Recognition and basic recallVariables, primitive data types
3-4Comprehension and guided applicationOperators, function declarations
5-6Application and analysisClosures, callbacks, promises
7-8Complex analysis and synthesisEvent loop, async patterns
9-10Multi-step transfer, novel problem solvingDesigning async architectures

Estimated minutes

How long you expect a typical student to spend mastering this concept, including instruction, worked examples, and practice. This calibrates course-level time estimates and XP awards. A good heuristic: difficulty 1-3 takes 10-20 minutes, difficulty 4-6 takes 20-35 minutes, difficulty 7+ takes 30-45 minutes.

Tags

Tags are free-form labels for cross-cutting concerns. Use them for filtering and analytics — for example, foundational, calculation, hands-on. Tags have no effect on the adaptive engine.

Source references

The sourceRef field traces a concept back to the authoritative source material. For certification courses, this is the exam guide section. For academic courses, the textbook chapter. Source references help with auditing and content updates.

7. Validate and Import

Before publishing, your course must pass schema validation and the 10-check review gate.

Schema validation

Run graspful validate to check that your YAML conforms to the schema — correct field types, required fields present, no cycles in the prerequisite graph.

graspful validate js-fundamentals.yaml

Review gate

The review gate runs 10 mechanical quality checks: YAML parsing, unique problem IDs, valid prerequisites, question deduplication, difficulty staircase, cross-concept coverage, variant depth, instruction formatting, worked example coverage, and import dry run. A score of 10/10 is required to publish.

# Run all 10 checks
graspful review js-fundamentals.yaml

# JSON output for CI pipelines
graspful review js-fundamentals.yaml --format json

See Review Gate for details on each check and how to fix failures.

Import and publish

Once you pass 10/10, import the course. Use --publish to go live immediately (the server re-runs the review gate before publishing).

# Import as draft
graspful import js-fundamentals.yaml --org my-org

# Import and publish in one step
graspful import js-fundamentals.yaml --org my-org --publish

8. Create Your Brand

A brand YAML configures the white-label product that students see: theme, landing page, pricing, SEO, and domain. Students never see Graspful — they see your brand.

Brand YAML

The brand YAML defines the product identity, theme preset, landing page copy, FAQ, pricing, and SEO metadata. See the Brand Schema for the full reference.

js-mastery-brand.yaml
brand:
  id: js-mastery
  name: JS Mastery
  domain: js-mastery.graspful.com
  tagline: "Master JavaScript from zero to async."
  orgSlug: my-org

theme:
  preset: amber
  radius: "0.5rem"

landing:
  hero:
    headline: "Learn JavaScript the Smart Way"
    subheadline: "Adaptive learning that focuses on what you don't know yet."
    ctaText: "Start Learning"
  features:
    heading: "Why JS Mastery?"
    items:
      - title: "Adaptive Engine"
        description: "Skips what you know. Focuses on your gaps."
        icon: Brain
      - title: "Spaced Repetition"
        description: "Never forget what you've learned."
        icon: Clock
      - title: "Real Problems"
        description: "Practice with code, not flashcards."
        icon: Code
  howItWorks:
    heading: "How It Works"
    items:
      - title: "Take a diagnostic"
        description: "20 questions to map what you already know."
      - title: "Learn adaptively"
        description: "The engine builds your personal learning path."
      - title: "Master JavaScript"
        description: "Prove mastery through progressive challenges."
  faq:
    - question: "How long does it take?"
      answer: "Most learners finish in 4-6 weeks studying 30 min/day."
    - question: "Do I need prior experience?"
      answer: "No. The course starts from variables and builds up."
  bottomCta:
    headline: "Ready to master JavaScript?"

seo:
  title: "JS Mastery — Adaptive JavaScript Learning"
  description: "Learn JavaScript with adaptive diagnostics and spaced repetition."
  keywords: [javascript, learn javascript, javascript course]

pricing:
  monthly: 19
  yearly: 149
  currency: usd
  trialDays: 7

contentScope:
  courseIds: [js-fundamentals]

Custom domain setup

By default, brands are served at your-brand.graspful.com. For a custom domain (e.g., learn.yourdomain.com), add a CNAME record pointing to brands.graspful.com and update the domain field in your brand YAML. SSL is provisioned automatically.

Launch checklist

  • Course: Imported and published (review gate 10/10)
  • Brand: Imported with landing page copy, theme, and pricing configured
  • Domain: CNAME record set (if using custom domain)
  • Stripe: Connect account linked for paid courses (see Billing)
  • Test: Go through the diagnostic as a student to verify the experience
# Import the brand
graspful import js-mastery-brand.yaml

# Verify everything is live
graspful describe --org my-org

Next steps

Course Creation Guide — Graspful Docs | Graspful