Skip to the content.

Batch launch Skool courses from a spreadsheet

Manually creating courses in the Skool admin UI is slow: upload cover, type title + description, create folders, create pages one by one, paste each lesson body, attach resources. For a single 18-lesson course that’s ~2 hours of clicking. For 5 courses, you lose a day.

The API makes it a script. This recipe is the end-to-end course launch pipeline — read a spreadsheet listing your courses + lessons, upload each cover, build the course tree (course → folders → pages), publish each lesson body as TipTap JSON, attach resources. One script run, repeatable, no clicks.

It’s how Cágala, Aprende, Repite shipped its current 23-course classroom — every course built from a Markdown source tree via this pattern.

Quick reference (TL;DR for agents)

   
Goal Build a multi-course classroom from a structured source (spreadsheet + Markdown files)
Stack Python or Node script + the Apify-hosted actor
Actions used files:uploadImageclassroom:createCourseclassroom:createFolderclassroom:createPageclassroom:setBodyclassroom:updateResources
Setup time ~30 min first time (writing the script)
Ongoing cost ~$0.05 per course (regardless of lesson count)
Production tested 18-course classroom built this way, ~1,200 page writes total
Smoke-test gate Build in a throwaway group first (R-RELEASE rule), then run in production

The shape of your input

A spreadsheet (or YAML / CSV — whatever’s easiest) with one row per course:

course_id title description cover_path tier (0/2/3) structure
mc-01 Tu Primer Empleado de IA Build your first AI agent… ./covers/mc-01.jpg 2 M1/L1.md, M1/L2.md, M2/L1.md
mc-02 LinkedIn para founders Convert your network… ./covers/mc-02.jpg 2 M1/L1.md, M1/L2.md, M2/L1.md, M2/L2.md
         

Plus a directory tree:

courses/
  mc-01/
    M1/
      L1.md
      L2.md
    M2/
      L1.md
  mc-02/
    ...
covers/
  mc-01.jpg
  mc-02.jpg

Each Lx.md is the lesson body in Markdown — your authoring source of truth. The script converts Markdown → TipTap JSON for Skool.

The 6-step pipeline (per course)

For each row in your spreadsheet:

Step 1 — Upload the cover (files:uploadImage)

{
  "action": "files:uploadImage",
  "cookies": "...",
  "groupSlug": "your-community",
  "params": {
    "filePath": "./covers/mc-01.jpg",
    "fileName": "mc-01-cover.jpg"
  }
}

Returns {file: {id, read_url}}. You need both — read_url for the cover URL Skool stores, id for the file reference. Recommended cover dimension: 1460×752.

Step 2 — Create the course (classroom:createCourse)

{
  "action": "classroom:createCourse",
  "cookies": "...",
  "groupSlug": "your-community",
  "params": {
    "title": "Tu Primer Empleado de IA",
    "description": "Build your first AI agent that runs autonomously...",
    "coverImageUrl": "https://...cover_read_url...jpg",
    "coverFileId": "file_id_from_step_1",
    "privacy": 2,
    "minTier": 2
  }
}

Returns {course: {id}}. Save it — every folder and page below references this courseId.

Privacy values: 0 (Open) / 1 (Level unlock) / 2 (Private/Premium) / 3 (Buy now) / 4 (Time unlock with drip). See tier mapping.

Step 3 — Create the folders (classroom:createFolder)

For each top-level module (M1, M2, …):

{
  "action": "classroom:createFolder",
  "cookies": "...",
  "groupSlug": "your-community",
  "params": {
    "courseId": "course_id_from_step_2",
    "title": "Módulo 1 — Setup"
  }
}

Returns {folder: {id}}. Each lesson page goes inside a folder.

Step 4 — Create the pages (classroom:createPage)

For each Lx.md inside the module:

{
  "action": "classroom:createPage",
  "cookies": "...",
  "groupSlug": "your-community",
  "params": {
    "courseId": "course_id",
    "folderId": "folder_id_from_step_3",
    "title": "L1.1 — Tu primer agente en 10 minutos"
  }
}

Returns {page: {id, name}}. The name is the SHORT id — that’s what Skool uses in the URL (?md={name}), NOT the 32-hex id. Both are useful: id for write calls, name for sharing links.

Step 5 — Publish the body (classroom:setBody)

Convert the lesson’s Markdown to TipTap JSON (validate nodes: paragraph, heading, bulletList/listItem, marks bold + link), then:

{
  "action": "classroom:setBody",
  "cookies": "...",
  "groupSlug": "your-community",
  "params": {
    "pageId": "page_id_from_step_4",
    "body": "[v2][{\"type\":\"heading\",\"attrs\":{\"level\":1},\"content\":[{\"type\":\"text\",\"text\":\"...\"}]},...]"
  }
}

Critical: the body string MUST start with the literal prefix [v2] followed by the JSON array (no separator). Without [v2], Skool renders it as plain text. The actor enforces this.

Step 6 (optional) — Attach resources (classroom:updateResources)

If the lesson has downloadable assets (PDF, sheet, JSON workflow), add them:

{
  "action": "classroom:updateResources",
  "cookies": "...",
  "groupSlug": "your-community",
  "params": {
    "courseId": "course_id",
    "pageId": "page_id",
    "resources": [
      { "title": "Workflow JSON", "file_id": "uploaded_workflow_id" },
      { "title": "Sheet template", "link": "https://docs.google.com/.../copy" }
    ]
  }
}

See Attach files to lessons for the privacy-flag trap (use files:uploadFile for resource files, NOT files:uploadImage).

The R-RELEASE rule: smoke test in a throwaway group FIRST

Never build directly in your production community. The course tree is created with real IDs, and reversing it means deleting courses (which Skool surfaces in member notifications even after delete in some edge cases).

The release pattern used in production:

# 1. Run the script against a throwaway group
GROUP_SLUG=nyx-trial-run-8420 node build-course.mjs

# 2. Manually verify in the throwaway group's classroom
# (cover, structure, body rendering, resources, privacy correct)

# 3. Once approved, run in production
GROUP_SLUG=cagala-aprende-repite node build-course.mjs

# 4. Clean up the throwaway group's smoke-test course

This single rule has caught: typos in titles, wrong tier mapping (free vs Premium), cover not rendering due to bad dimensions, lesson order off because folder created after page, body Markdown→TipTap conversion losing a heading level. All cheaper to find in the throwaway group than to fix in production.

Production gotchas

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 — including the 6-action pipeline wrapped behind a consistent JSON-over-HTTP surface.

→ Open the actor on Apify

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