SuperAPI AI Directory

iblai-api-studio-outline

Office & productivity

View repository

Updated 10-15 22:16

About this skill

iblai-api-studio-outline

The IBL course-creator app on Studio adds api/v1/ibl/… routes that wrap the stock /xblock/ handler for bulk work. create_full_course walks a JSON spec — sections → subsections → units → components — creates everything inside one modulestore transaction and publishes the whole course at the end. A five-section course with text and quizzes becomes one request instead of a hundred.

Before you start

  1. Three situations, three tools. New, empty course → create_full_course (below; it appends and then publishes the entire course, drafts included). One section, lesson, unit or component of an existing course → the individual skills (/iblai-api-studio-section, -subsection, -unit, -html, -problem, -pdf), which act on one locator. Many changes to an existing course (rewrite readings, add units across lessons, restructure) → the reconcile driver (section "Bulk edits to an existing course" below), which diffs the same spec against the live outline and only touches what differs.
  2. Preflight: node .claude/skills/iblai-api-studio-auth/scripts/studio-login.mjs --check must print two oks — otherwise run /iblai-api-studio-auth first, do not attempt the calls below.
  3. A course key from /iblai-api-studio-course-create; the user must be instructor on it (the creator is). For create_full_course, confirm with GET /xblock/outline/{ROOT} that child_info.children is empty; for the reconcile driver, that outline is the baseline it diffs against.
  4. Settings and grading first (/iblai-api-studio-settings, /iblai-api-studio-grading): this call publishes, and pacing cannot change after the course start.
  5. The outline planned per /iblai-api-studio "Planning a production-grade outline" and reviewed with the user.
  6. If sub-agents wrote parts of the spec: they worked from the same content brief, and every fragment has been reviewed against it (/iblai-api-studio "Content quality") — consistent voice and headings, no thin or placeholder units, valid OLX, titles on every block. The builder posts whatever it is given and publishes it; divergent or shallow content goes live verbatim.

Auth & conventions

  • Base URL: $STUDIO_URL; Studio session cookies from studio.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="course-v1:<org>+<number>+<run>"; ROOT="block-v1:<org>+<number>+<run>+type@course+block@course".
  • Responses use xblock_locator (v1 routes) where the stock handler says locator.
  • Do not send position on the v1 create route: it returns 500 on this deployment. Order is creation order; reorder afterwards with children (/iblai-api-studio-section).
  • The v1 course/asset/upload route also returns 500 — upload assets with POST /assets/{COURSE}/ (/iblai-api-studio-settings).
  • Creating content is not reversible in bulk (there is no undo; delete blocks one by one) — show the user the spec and confirm before posting.

Reads

  • GET /api/v1/ibl/course/{COURSE}/units → { "units": [ { "id": "<unit locator>", "display_name", "type": "vertical", "children": [ { "id", "display_name", "type": "html" | "problem" | "pdf" | … } ] } ] } — every unit in outline order with its components, straight from the modulestore (no cache lag). The cheapest way to get all locators after a bulk build.
  • GET /api/v1/ibl/xblock/get-details?locator=<url-encoded usage key> → { "id", "data", "display_name" } for html (and video) blocks; 406 for other types.
  • Full tree with publish state: GET /xblock/outline/{ROOT} (/iblai-api-studio-section).

Writes

  • POST /api/v1/ibl/course/create_full_course — build and publish the tree. Confirm with the user first.
    { "course_parent_locator": "<ROOT>",
      "section": [
        { "name": "Week 1 · Foundations",
          "subsection": [
            { "name": "1.1 What is data?",
              "unit": [
                { "name": "Overview",
                  "problems": [
                    { "problem_type": "html", "data": "<section><h3>Why data matters</h3><p>…</p></section>" },
                    { "problem_type": "video", "metadata": { "display_name": "Intro video", "youtube_id_1_0": "dQw4w9WgXcQ", "html5_sources": [] } }
                  ] },
                { "name": "Practice",
                  "problems": [
                    { "problem_type": "blank",
                      "data": "<problem>…OLX with one or more responses…</problem>",
                      "metadata": { "display_name": "Check your understanding", "max_attempts": 3, "weight": 2, "showanswer": "finished", "markdown": null } }
                  ] }
              ] }
          ] }
      ] }
    
    → 200 { "course_parent_locator": "<ROOT>", "created_units": [ { "locator": "<unit>", "display_name" } ] }. Keys are exactly section[].name, subsection[].name, unit[].name, unit[].problems[]; every level is required (a unit may have "problems": []). problems[].problem_type: html (data = HTML fragment), blank (a problem block: data = OLX, metadata as in /iblai-api-studio-problem, optional boilerplate), multiplechoice (same, seeded from the multiple-choice boilerplate), dropdown (same, boilerplate: "optionresponse.yaml"), video (metadata.youtube_id_1_0, html5_sources), drag-and-drop-v2, lti_consumer, ibl_openedx_scorm_xblock. pdf is not supported — add PDFs afterwards with /iblai-api-studio-pdf. Always put "markdown": null in a problem's metadata so the OLX is authoritative. Limits: html components are created with the default title "Text" (the builder ignores display_name for html) — rename them afterwards with the update route below or the section rhythm will look uniform; the whole course is published on success, including previously unpublished drafts. Errors: 400 (serializer message) for a missing course_parent_locator; 403 "You do not have permission to create content in this course"; a 500 HTML page usually means a malformed spec (e.g. a missing problems key) — nothing before the failing block is rolled back, so read the outline before retrying.
  • POST /api/v1/ibl/xblock/create { "category": "chapter" | "sequential" | "vertical" | "html" | "problem", "parent_locator": "<parent>", "display_name": "…", "boilerplate": "…" } → { "xblock_locator": "…" }. Same as POST /xblock/; no position.
  • POST /api/v1/ibl/xblock/update { "locator": "<block>", "category": "<its category>", "courseKey": "<COURSE>", "data": "…", "metadata": { "display_name": "…", … } } → 200 (empty). Top-level display_name is ignored — put it in metadata.
  • POST /api/v1/ibl/xblock/publish { "locator": "<block>" } → 200 (empty). Same as { "publish": "make_public" } on /xblock/{block}.

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"

# spec.json written from the reviewed plan; validate every OLX string first
python3 - <<'PY'
import json, xml.dom.minidom
spec = json.load(open("spec.json"))
for s in spec["section"]:
    for ss in s["subsection"]:
        for u in ss["unit"]:
            for p in u["problems"]:
                if p["problem_type"] in ("blank", "multiplechoice", "dropdown"):
                    xml.dom.minidom.parseString(p["data"])
print("spec ok:", sum(len(ss["unit"]) for s in spec["section"] for ss in s["subsection"]), "units")
PY
curl "${S[@]}" -X POST "$STUDIO_URL/api/v1/ibl/course/create_full_course" -d @spec.json > build.json
python3 -c 'import json; print(len(json.load(open("build.json"))["created_units"]), "units created")'

# rename the html blocks the builder titled "Text"
curl "${S[@]}" "$STUDIO_URL/api/v1/ibl/course/$COURSE/units" | python3 -c '
import json, sys, subprocess
for u in json.load(sys.stdin)["units"]:
    for c in u["children"]:
        if c["type"] == "html" and c["display_name"] == "Text":
            print(c["id"])' | while read -r ID; do
  curl "${S[@]}" -o /dev/null -X POST "$STUDIO_URL/api/v1/ibl/xblock/update" \
    -d "{\"locator\":\"$ID\",\"category\":\"html\",\"courseKey\":\"$COURSE\",\"metadata\":{\"display_name\":\"Reading\"}}"
done

After a bulk build — the three fix-up passes

create_full_course does not finish the course. Two things it cannot express, plus the republish they cause, are always needed before the outline is done. Budget for them; they are pure derivation from the spec you already have.

  1. Rename the html components. The builder titles every html block "Text" (it ignores display_name for html; problem blocks keep their metadata.display_name and need nothing). Rename them with POST /api/v1/ibl/xblock/update — the loop in the Example above does it for a whole course. Leave this out and every reading is called "Text" in the unit header, the breadcrumb and analytics.
  2. Set the graded subsections. The spec has no graderType key — subsection[] accepts only name and unit[]. Every graded subsection is notgraded until you POST /xblock/{subsection} {"graderType": "Homework"} per /iblai-api-studio-subsection. Read the tree back with GET /xblock/outline/{ROOT} to get the subsection locators.
  3. Publish again. The builder publishes at the end of its request, so the two passes above leave the course dirty (has_changes: true). Finish with POST /xblock/{ROOT} {"publish": "make_public"} and confirm the root reads published: true, has_changes: false.

Verify the finished outline in one read: GET /xblock/outline/{ROOT} gives course_graders plus every subsection's graded / format and publish state.

Bulk edits to an existing course — reconcile

scripts/reconcile.py (standard library only) takes the same spec shape and makes the live course match it: creates what is missing, updates components whose data/metadata differ, reorders, reports blocks that are live but not in the spec (deletes them only with --delete), and publishes changed units with --publish. Idempotent — a second run is a no-op. Dry run by default.

python3 .claude/skills/iblai-api-studio-outline/scripts/reconcile.py spec.json --course "$COURSE"                       # plan only
python3 .claude/skills/iblai-api-studio-outline/scripts/reconcile.py spec.json --course "$COURSE" --apply --publish     # do it
python3 .claude/skills/iblai-api-studio-outline/scripts/reconcile.py spec.json --course "$COURSE" --apply --delete      # also remove blocks absent from the spec — confirm with the user first
  • Matching: by "id" (a locator) when a node carries one, else by name at the same level (section/subsection/unit name, component display_name
    • type). Rename = delete + create, so give nodes an id (from build.json) before renaming.
  • Spec additions over create_full_course: subsection.graderType, and display_name on every component (required — it is the match key). problem_type handled: html, blank, multiplechoice, dropdown, video; pdf is skipped with a note (/iblai-api-studio-pdf).
  • Reconciling a course that was bulk-built. Components match on (type, display_name), and create_full_course leaves every html block named "Text". The driver treats a live block that still carries a default title ("Text", "Multiple Choice", "Video", …) as unnamed and adopts it for the next spec component of the same type in that unit, then sets the spec's display_name on it — so the first reconcile after a bulk build renames the readings instead of duplicating them (fix-up pass 1 becomes part of the reconcile). Two cautions: the adoption is positional within a unit, so keep the spec's component order equal to the built order until the first reconcile has run; and a spec component with no display_name falls back to the literal problem_type as its name — always give components a title. Pin a component with an "id" from build.json when in doubt.
  • Plan lines read create | update | reorder | grade | delete | extra | publish <path> [locator]; --apply writes build.json (path → locator).
  • Cost: 3 calls for the plan (outline + units + nothing else) plus one GET per matched component, then one call per change. Rewriting 40 readings ≈ 45 calls.
  • Flags: --env studio.env, --map build.json. Exit with the failing call's status on any HTTP error (302/403/500 → /iblai-api-studio-auth).

Workflow for "rewrite the readings and add units": export the live outline into a spec (GET /xblock/outline/{ROOT} + GET /api/v1/ibl/course/{COURSE}/units, or reuse the previous spec.json), edit it, dry-run, review the plan with the user, --apply --publish.

Same thing, three names

Endpoint locator title type
stock /xblock/… create / outline locator (create) · id (read) display_name category
v1 xblock/create · course/{COURSE}/units xblock_locator (create) · id (units) display_name type
/api/contentstore/v1/container/vertical/{unit}/children block_id name block_type

Notes

  • Keep spec.json and build.json next to the project: together they are the resumable record of what exists (re-running the bulk call duplicates every section; the reconcile driver does not).
  • One blank problem per unit with several *response elements is the intended shape; several problem blocks in one unit force separate submissions (/iblai-api-studio-problem).
  • The builder publishes; if the user wants to review before going live, build with the block-by-block skills instead, or build into a course whose start_date is in the future and visible_to_staff_only is set on sections.
  • Stock equivalents: POST /xblock/ (/iblai-api-studio-section), POST /xblock/{block} (/iblai-api-studio-html), { "publish": "make_public" } (/iblai-api-studio-publish).