A course outline is a tree: course → section (chapter) → subsection
(sequential) → unit (vertical) → components. Sections are the top level.
This skill covers sections and the outline read; subsections and units have
their own skills with the same /xblock/ mechanics.
Before you start
- Preflight:
node .claude/skills/iblai-api-studio-auth/scripts/studio-login.mjs --checkprints twooks; otherwise run/iblai-api-studio-authfirst — do not attempt the calls below. - A course key from
/iblai-api-studio-course-create(→ROOTbelow) and the outline planned per/iblai-api-studio. - Building a whole new outline?
/iblai-api-studio-outlinecreates every section/subsection/unit in one call; use this skill for edits and additions.
Auth & conventions
- Base URL:
$STUDIO_URL; Studio session cookies 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") - Keys: course root
ROOT=block-v1:<org>+<number>+<run>+type@course+block@course; a section isblock-v1:…+type@chapter+block@<hex>(thelocatorreturned on create). - Edits land in the draft; users see them only after
/iblai-api-studio-publish. - DELETE is destructive (removes every subsection/unit inside) — confirm with the user first.
Reads
- GET
/xblock/outline/{ROOT}— the full outline, sections → subsections → units (components are not included). Each node:{id, display_name, category, has_children, published, published_on, has_changes, visibility_state, released_to_students, release_date, start, due, format, graded, has_explicit_staff_lock, studio_url, lms_url, actions, child_info: {category, display_name, children: […]}}.visibility_state∈live·ready(published, release date in the future) ·unscheduled·needs_attention(unpublished changes) ·staff_only·gated. The root also carriescourse_graders(assignment types from the grading policy) andstart. - GET
/xblock/outline/{section}— the same for one section and its descendants. - GET
/xblock/{section}— the section's own fields (metadata,published,has_changes,start,visibility_state), no children.
Writes
- POST
/xblock/— create a section:
→{ "parent_locator": "<ROOT>", "category": "chapter", "display_name": "Week 1 — Foundations" }{ "locator": "block-v1:…+type@chapter+block@<hex>", "courseKey": "course-v1:…" }. New sections append at the end. - POST
/xblock/{section}— update fields; send only what changes:
→{ "metadata": { "display_name": "Week 1", "start": "2026-10-01T00:00:00Z", "visible_to_staff_only": true } }{ "id", "data": null, "metadata": {…echo of the stored metadata…} }.start= release date (ISO 8601 UTC; users cannot open it before).visible_to_staff_only: truehides it from users even when published (falseor"nullout": ["visible_to_staff_only"]to clear). Optional:"metadata": {"highlights": ["Topic A", "Topic B"]}for weekly highlight emails. - POST
/xblock/{ROOT}{ "children": ["<section A>", "<section B>", …] }— reorder sections (the list must contain every current section id). Sections cannot be moved with themove_source_locatorPATCH (400 "You can not move chapter into course"); reorder this way. - POST
/xblock/{ "duplicate_source_locator": "<section>", "parent_locator": "<ROOT>" }— duplicate a section with everything inside →{locator, courseKey}. - DELETE
/xblock/{section}→204. Confirm with the user first. - POST
/xblock/{section}{ "publish": "make_public" }— publish the section and all its descendants (details anddiscard_changesin/iblai-api-studio-publish).
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="course-v1:acme+DATA101+2026-T1"; ROOT="block-v1:${COURSE#course-v1:}+type@course+block@course"
SECTION=$(curl "${S[@]}" -X POST "$STUDIO_URL/xblock/" \
-d "{\"parent_locator\":\"$ROOT\",\"category\":\"chapter\",\"display_name\":\"Week 1\"}" \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["locator"])')
curl "${S[@]}" -X POST "$STUDIO_URL/xblock/$SECTION" -d '{"metadata":{"start":"2026-10-01T00:00:00Z"}}' >/dev/null
curl "${S[@]}" "$STUDIO_URL/xblock/outline/$ROOT" | python3 -c '
import sys,json
for s in json.load(sys.stdin)["child_info"]["children"]:
print(s["display_name"], s["visibility_state"], "changes" if s["has_changes"] else "", s["id"])'
Notes
- Creating a section publishes nothing; a brand-new section reads
published: true, has_changes: falseonly because it is empty — its content will need publishing. - The outline call is the cheapest way to get every id at once; cache the ids you get back from creates instead of re-reading after each call.
- Dates are UTC ISO strings; Studio echoes them back normalized (
2026-10-01T00:00:00Z). - Course-level
start(in/iblai-api-studio-settings) gates everything: a section release date earlier than the course start is still not visible. - Next:
/iblai-api-studio-subsection(lessons inside this section).