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:createPage → classroom: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
- Apify token (sign up free)
- Skool admin cookies (see Authentication)
- An existing course with a folder ready to receive pages (use classroom:createCourse + createFolder if you don’t have one)
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
- The
[v2]prefix is non-negotiable. Without it, your body renders as plain text including the JSON itself — looks broken to members. The actor doesn’t add it for you onsetBodybecause some callers want raw text. Always prefix programmatically. - TipTap node validation. Skool rejects unknown nodes silently — the page renders without them. Stick to the validated set above. If you have markdown with images, tables, code blocks: convert images to upload-then-link, tables to lists, code blocks to
<pre>-styled paragraphs. - Rate limit on writes. Skool’s ceiling is ~25 writes/min. The actor serializes internally. If you’re building 50+ lessons in one run, expect ~2 minutes of wall time.
- Folder must exist before pages.
createPagewith a non-existentfolderIdreturns 400. If your spec lists a new folder, create it first withclassroom:createFolder. page.nameis generated by Skool from the title. Two pages with identical titles get suffixes (-2,-3). If you need predictable URLs, ensure unique titles in your spec.
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
- Recipe: Batch launch courses from a spreadsheet — when your spec grows
- Recipe: Publish a course from Markdown — single-course version of the spreadsheet pattern
- Recipe: Attach files to lesson pages — the resources step in detail
- Classroom API reference — every action with params
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.
- Pay-per-event pricing (~$1.50/mo for typical communities)
- The classroom action set is the most production-tested part of the actor — 1,200+ page writes in the wild
- The
[v2]prefix, page-name generation, and folder validation are all surfaced clearly via the action params
New to Skool? Launch your community here — 14-day free trial.