Create the course shell that every other /iblai-api-studio-* skill fills.
One call, one course key. Studio on ibl.ai exposes a management endpoint that
also registers the course in the ibl.ai catalog and gives the creator the
course-instructor role — use it, not the stock Studio route.
Before you start
- Preflight:
node .claude/skills/iblai-api-studio-auth/scripts/studio-login.mjs --checkprints twooks andstudio.envhasSTUDIO_ORG=<org key>. Anything else →/iblai-api-studio-authfirst; never guess an org key and never usemain. - The user's roles include
org-instructorfor that org (GET /api/ibl/users/manage/roles/?username=$STUDIO_USERNAME); otherwise/iblai-api-managementgrants it. - Agree
numberandrunwith the user — they are part of the key forever.
Auth & conventions
- Base URL:
$STUDIO_URL; Studio session cookies + CSRF fromstudio.env(its values are single-quoted — bash strips the quotes; any other parser must strip them too, or Studio answers 500:/iblai-api-studio-auth). - Snippet:
set -a; . ./studio.env; set +a S=(-s -b "studio_session_id=$STUDIO_SESSION; csrftoken=$STUDIO_CSRF" -H "X-CSRFToken: $STUDIO_CSRF" \ -H "Origin: $STUDIO_URL" -H "Referer: $STUDIO_URL/" -H "Accept: application/json" -H "Content-Type: application/json") - Course keys in query strings must be URL-encoded; in paths they may be raw.
- Creating registers the course in the catalog at once; deleting is permanent and destroys user data — see the guard rails under Writes. Confirm both with the user first.
What a good course looks like
The shell you create here is judged by what fills it. Before building, agree
this shape with the user (details in /iblai-api-studio "Planning a
production-grade outline"):
- Planned, not improvised. 3–6 learning objectives; every section serves one; graded work assesses one. Write the outline (sections → subsections → units → components) down first and have the user review it.
- An intro and an outro. First section: welcome, objectives, how the course works, how grading works, syllabus (PDF). Last section: summary, further reading, feedback and next steps. Learners orient at the start and consolidate at the end.
- Well labelled. One naming pattern at every level (
Week 2 · Reading data critically→2.1 Distributions→Overview/Reading/Practice/Summary), unit titles that say what the page is, component titles that are never left as "Text" or "Multiple Choice". - A repeatable unit rhythm. Overview → content (html, video, pdf) → one practice block holding all the unit's questions → summary. Same rhythm in every lesson.
- Readable pages. Short paragraphs with space between them, headings that announce each part, callouts for key ideas, lists for lists, one idea per unit, readings under ~1,500 words, videos under 10 minutes. HTML blocks wrapped in a
<section>with the spacing rules of/iblai-api-studio-html. - Assessment with feedback. Hints on choices, a solution on every question, formative checks inside lessons, summative work in graded subsections whose type exists in the grading policy.
- One voice, even when many hands write it. Sub-agents drafting sections or units diverge in tone, depth and structure unless they share one content brief and every piece is reviewed against it before posting (
/iblai-api-studio"Content quality"). Well written, presentable, beautiful, informative without distraction, carefully planned and reviewed — for every unit, not on average. - Configured before it goes live. Dates, pacing, enrollment window, description and images set (
/iblai-api-studio-settings), grading policy set, outline complete — then one publish.
Reads
- GET
/api/contentstore/v2/home/courses?page=1&search=<text>— the signed-in user's courses (paginated):{count, num_pages, next, previous, results: {courses: [{course_key, display_name, org, number, run, url, cms_link, lms_link, rerun_link, is_active}], in_process_course_actions: []}}.searchmatches the display name. (/api/contentstore/v1/home/coursesreturns the same unpaginated as{courses, archived_courses}.) - GET
/api/ibl/manage/course/?course_key=<url-encoded key>— one course's summary:{course_key, org, number, run, display_name, url}.400 {"error":"course_key parameter is required"}without the param;404if unknown. - GET
/xblock/outline/block-v1:<org>+<number>+<run>+type@course+block@course— the course root as Studio sees it (display_name,published,has_changes,start,course_graders,child_info.children= sections). Detailed in/iblai-api-studio-section. - GET
/api/ibl/users/manage/roles/?username=$STUDIO_USERNAME— the user's edX roles:[{role, org, course}]. Creating needsorg-instructor(orcourse-creator/org-course-creator) for the target org.
Writes
-
POST
/api/ibl/manage/course/— create a course. Confirm with the user first.{ "org": "acme", "display_name": "Intro to Data", "number": "DATA101", "run": "2026-T1" }organddisplay_nameare required.numberandrunare optional: when omitted Studio assignsC<n>(next free number in the org) and the currentYYYY-MM. Response200:{ "url": "/course/course-v1:acme+DATA101+2026-T1", "course_key": "course-v1:acme+DATA101+2026-T1" }Side effects: the caller becomes
course-instructor+course-staffon it, is enrolled, and the course is registered in the ibl.ai catalog for that org (it shows in the LMS app's catalog immediately, empty and unpublished).Errors:
401no/expired session →/iblai-api-studio-auth;403 {"error": "Permission denied"}— the user cannot create courses in that org, ororgis missing → grantorg-instructorvia/iblai-api-management;400 {"ErrMsg": "There is already a course defined with the same organization and course number…", "OrgErrMsg", "CourseErrMsg"}—org+number+runexists (pick a new run, or reuse the existing key). -
POST
/api/ibl/manage/course/delete— DANGER: deletes the course, its content, its users' enrollments and progress, permanently and without undo. In production this is data loss. Do not run it because it seems convenient; treat it as a last resort that the user must ask for explicitly. Required before calling, all of them:- The user asked to delete (not "remove from catalog", "hide", "archive" — those are
/iblai-api-studio-settings:catalog_visibility: "none",visible_to_staff_onlyon sections, or anend_datein the past). GET /api/ibl/manage/course/?course_key=…returns the course and itsdisplay_namematches what the user named; show both to the user.- Check for enrolled users:
POST $LMS_URL/courses/{COURSE}/instructor/api/get_students_features(/iblai-api-studio-lms) — if anyone besides the author is enrolled, stop and report the count; deletion needs an explicit decision from the user with that number in front of them. - The user confirms by writing the full course key back (not "yes"); on the production host (
studio.learn.iblai.app) ask a second time. - Never delete a course you did not create in this session unless the user has done steps 1–4; never delete more than one course per confirmation.
{ "course_key": "course-v1:acme+DATA101+2026-T1", "keep_instructors": true, "remove_assets": false }→
200 {"message": "course with id: course-v1:… deleted"}. Needsorg-instructorfor the course's org (or global staff).404 {"error":"Course key does not exist"},400for a missing/invalid key,403without the role. Keepremove_assets: false(files may be referenced elsewhere) andkeep_instructors: trueunless told otherwise. Report the deletion in the final summary. - The user asked to delete (not "remove from catalog", "hide", "archive" — those are
-
The stock Studio
POST /course/({org, number, run, display_name}) exists but answers403 "User does not have the permission to create courses in this organization"for org instructors; only use it for global course creators, and prefer the endpoint above because of its catalog registration.
Example
set -a; . ./studio.env; set +a
S=(-s -b "studio_session_id=$STUDIO_SESSION; csrftoken=$STUDIO_CSRF" -H "X-CSRFToken: $STUDIO_CSRF" \
-H "Origin: $STUDIO_URL" -H "Referer: $STUDIO_URL/" -H "Accept: application/json" -H "Content-Type: application/json")
COURSE=$(curl "${S[@]}" -X POST "$STUDIO_URL/api/ibl/manage/course/" \
-d "{\"org\":\"$STUDIO_ORG\",\"display_name\":\"Intro to Data\",\"number\":\"DATA101\",\"run\":\"2026-T1\"}" \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["course_key"])')
echo "$COURSE" # course-v1:acme+DATA101+2026-T1
ROOT="block-v1:${COURSE#course-v1:}+type@course+block@course" # parent_locator for sections / the bulk builder
Notes
- Derive the course root block as above; it is the
parent_locatorfor/iblai-api-studio-sectionand/iblai-api-studio-outline, and the target for a whole-course publish. numberandrunbecome part of the key forever: letters, digits,.,_,-only, no spaces. Use a run that encodes the cohort (2026-T1,self-paced).- A new course starts with
start=2030-01-01(never released) and no content: set real dates, pacing and enrollment window in/iblai-api-studio-settingsbefore the first publish. - Re-running with the same
number+runfails; list first and reuse the key if the course already exists. - LMS app URL to report:
$LMS_APP_URL/platform/<org>/courses/<course_key>. - AI-generated course drafts through the ibl.ai pipeline:
/iblai-vibe-course-create.