Skip to the content.

Generate N Skool lessons from a JSON spec

The Batch launch courses from a spreadsheet recipe assumes you’re reading Markdown files from disk. This recipe is the smaller, faster cousin: when your lesson content is structured (templated questions, generated copy, AI-drafted bodies), you can skip the file system entirely and drive everything from a JSON spec.

The use case: building 5-10 similar lessons that share structure but vary in content. Onboarding sequences, weekly “what we shipped” updates, AI-generated mini-courses, certifications with N sub-lessons. Write the JSON once, run the script, get the course built.

Quick reference (TL;DR for agents)

   
Goal Generate N lesson pages from a JSON spec in one script run
Stack Any HTTP client + the Apify-hosted actor
Actions used classroom:createPageclassroom:setBody (looped)
Setup time ~10 min (writing the spec + the loop)
Ongoing cost $0.01 × N lessons created (~$0.10 for a 10-lesson course)
Best for 5-20 similar lessons (>20 → use the spreadsheet pattern)
Key gotcha TipTap body MUST start with [v2] prefix or it renders as plain text

When to use which approach

Pattern Best when
Spreadsheet + Markdown files Authoring in your IDE, version control, manual editing per lesson
JSON spec (this recipe) Programmatic generation: AI-drafted, templated, weekly recurring
Skool admin UI Single one-off lesson — the API isn’t worth the setup

Prerequisites

The spec shape

A flat JSON list of lessons, each with title, body (TipTap or Markdown→TipTap output), and optional resources:

{
  "courseId": "course_32hex",
  "folderId": "folder_32hex",
  "lessons": [
    {
      "title": "L1 — What is automation?",
      "body": "[v2][{\"type\":\"heading\",\"attrs\":{\"level\":1},\"content\":[{\"type\":\"text\",\"text\":\"What is automation?\"}]},{\"type\":\"paragraph\",\"content\":[{\"type\":\"text\",\"text\":\"...body text...\"}]}]"
    },
    {
      "title": "L2 — Your first webhook",
      "body": "[v2]...",
      "resources": [
        { "title": "Workflow JSON", "file_id": "uploaded_file_id" }
      ]
    }
  ]
}

Step 1 — Convert your source to TipTap

If you author in Markdown (or have AI generate Markdown), convert to TipTap JSON before building the spec. Validate nodes against the supported set: paragraph, heading (attrs.level), bulletList/listItem, orderedList/listItem, marks bold + link (attrs.href).

Quick Node converter (using marked + a small adapter):

import { marked } from 'marked';
import { mdToTiptap } from './md-to-tiptap.js'; // see Markdown→TipTap recipe

function toBody(markdown) {
  const tokens = marked.lexer(markdown);
  const tiptap = mdToTiptap(tokens);
  return '[v2]' + JSON.stringify(tiptap);
}

(There’s no canonical converter — the batch-create-courses-spreadsheet recipe has a Python reference impl.)

Step 2 — Loop: createPage + setBody (+ optional resources)

For each lesson in the spec:

# Create the page (returns page.id + page.name)
curl -X POST "https://api.apify.com/v2/acts/cristiantala~skool-all-in-one-api/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"action\": \"classroom:createPage\",
    \"cookies\": \"$COOKIES\",
    \"groupSlug\": \"your-community\",
    \"params\": {
      \"courseId\": \"$COURSE_ID\",
      \"folderId\": \"$FOLDER_ID\",
      \"title\": \"$LESSON_TITLE\"
    }
  }"

# Set the body (returns success)
curl -X POST "..." \
  -d "{
    \"action\": \"classroom:setBody\",
    \"cookies\": \"$COOKIES\",
    \"groupSlug\": \"your-community\",
    \"params\": {
      \"pageId\": \"$PAGE_ID\",
      \"body\": \"$TIPTAP_BODY\"
    }
  }"

Python loop:

for lesson in spec["lessons"]:
    page = run_action("classroom:createPage", {
        "courseId": spec["courseId"],
        "folderId": spec["folderId"],
        "title": lesson["title"]
    })
    run_action("classroom:setBody", {
        "pageId": page["page"]["id"],
        "body": lesson["body"]
    })
    if "resources" in lesson:
        run_action("classroom:updateResources", {
            "courseId": spec["courseId"],
            "pageId": page["page"]["id"],
            "resources": lesson["resources"]
        })

Step 3 — Verify and capture page URLs

After the loop, fetch classroom:getTree and pull the SHORT name field for each page — that’s the URL slug Skool uses (?md={name}), not the 32-hex id:

{
  "action": "classroom:getTree",
  "cookies": "...",
  "groupSlug": "your-community",
  "params": {}
}

Save the URLs to share with members:

https://www.skool.com/your-community/classroom/{course.name}?md={page.name}

Production gotchas

When you’ve outgrown this pattern

If your spec exceeds ~20 lessons or includes covers/folders/multi-course logic, switch to the Batch launch courses from a spreadsheet pipeline — it handles the upload-cover + create-course + multi-folder orchestration this recipe deliberately skips.

See also


Use this in production — no setup

The hardest part of building Skool automation isn’t the API logic — it’s the auth (cookies expire every ~3.5 days, WAF token rotation, weekly Skool buildId changes). The Skool All-in-One API actor on Apify handles all of that.

→ Open the actor on Apify

New to Skool? Launch your community here — 14-day free trial.