---
title: "A Course Authoring Agent That Builds Canvas Courses End to End"
slug: "canvas-course-builder-agent"
date: "2026-07-29"
tag: "Application"
summary: "A new Course Authoring agent takes a course description in plain English and assembles the whole thing in Canvas — shell, pages, assignments, quizzes, modules, module items — in dependency order, unpublished, idempotent, and ready for a human to review before students ever see it."
author: "ibl.ai Engineering"
linkedin: |
  Canvas has no "create a course" endpoint.

  A course is a dozen independent resources — shell, sections, assignment groups, pages, assignments, discussions, quizzes, modules, module items, enrollments — each with its own publish state. Wire them in the wrong order and you backfill IDs. Publish in the wrong order and you email real students. Miss one flag and you ship a course that looks perfect in the admin view and is completely invisible to the people taking it.

  We built an agent that knows all of that.

  You describe the course in a sentence. It builds the shell, authors the pages with real visual hierarchy, creates the assignments and quizzes, assembles the modules, wires every item, and stops — leaving the course unpublished with a one-line command to go live when you're ready.

  Two things we care about more than the demo:

  It writes a manifest. Run it twice and you get one course, not two — the second run updates in place instead of duplicating every page and module.

  It builds unpublished by design. Publishing a course with enrollments sends notifications to real people. There is no undo on that. So the agent assembles everything, publishes the modules, and hands the last step to a human.

  And you can then keep talking to it. "That page is too short" — it rewrites the page in place.

  The whole capability is a Markdown skill file you can open and edit in the platform. Not a plugin, not a deployment. Prose that the agent reads.

  #iblai #AgenticAI #EnterpriseAI #AI #Canvas #LMS #CourseDesign
---

<a href="/images/updates/canvas-course-builder-agent.png" target="_blank" rel="noopener noreferrer"><img src="/images/updates/canvas-course-builder-agent.png" alt="The Agentic OS chat panel on the left with a prompt asking the Course Authoring agent to create a course titled Your LMS needs AI, and the Canvas dashboard on the right showing published and unpublished courses." width="2820" height="1584" /></a>

We've shipped a **Course Authoring agent** that builds Canvas courses through the Canvas REST API — the whole course, not a single resource at a time.

You describe the course in plain English. The agent creates the shell, authors the pages, creates the assignments and quizzes, assembles the modules, wires every item into place, and hands you back a course that a human can review before anyone takes it.

The prompt in the screenshot above is the entire input: a title, what the course should teach, a note about visual hierarchy, and a cap of four modules.

## Watch it build a course

<div style="position:relative; width:100%; padding-bottom:56.25%; margin:1.5rem 0;">
  <iframe src="https://www.youtube.com/embed/YoVuFZp_qjo" title="Building a Canvas course with an AI agent" style="position:absolute; top:0; left:0; width:100%; height:100%; border:0;" allowfullscreen loading="lazy"></iframe>
</div>

## Why this is harder than it looks

Canvas has **no "create a whole course" endpoint.** A course is assembled from a dozen independent resources, and each one carries its own publish state.

The API will happily let you build something that looks complete in the admin view and is entirely invisible to students. A published assignment inside an unpublished module shows up for the instructor and for nobody else.

Most of the difficulty is **ordering, publish state, and idempotency** — not the individual HTTP calls. Those three are exactly what the agent encodes.

## The build order

Dependencies run one direction. Follow it and you never have to backfill an ID:

<table style="width:100%; border-collapse:collapse; margin:1.5rem 0; font-size:0.95rem;">
  <thead>
    <tr style="background:#f5f5f0; border-bottom:2px solid #2175C5;">
      <th style="text-align:left; padding:0.75rem; color:#5f6368; width:3rem;">#</th>
      <th style="text-align:left; padding:0.75rem; color:#5f6368;">Step</th>
      <th style="text-align:left; padding:0.75rem; color:#5f6368;">Why it's here</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom:1px solid #e5e7eb;">
      <td style="padding:0.75rem; font-variant-numeric:tabular-nums;">1</td>
      <td style="padding:0.75rem;"><strong>Course shell</strong></td>
      <td style="padding:0.75rem;">Created unpublished. Everything else hangs off its ID.</td>
    </tr>
    <tr style="border-bottom:1px solid #e5e7eb;">
      <td style="padding:0.75rem; font-variant-numeric:tabular-nums;">2</td>
      <td style="padding:0.75rem;"><strong>Sections</strong></td>
      <td style="padding:0.75rem;">Enrollments later need somewhere to land.</td>
    </tr>
    <tr style="border-bottom:1px solid #e5e7eb;">
      <td style="padding:0.75rem; font-variant-numeric:tabular-nums;">3</td>
      <td style="padding:0.75rem;"><strong>Assignment groups</strong></td>
      <td style="padding:0.75rem;">Assignments need a group to belong to.</td>
    </tr>
    <tr style="border-bottom:1px solid #e5e7eb;">
      <td style="padding:0.75rem; font-variant-numeric:tabular-nums;">4</td>
      <td style="padding:0.75rem;"><strong>Files</strong></td>
      <td style="padding:0.75rem;">A three-step upload, before anything links to them.</td>
    </tr>
    <tr style="border-bottom:1px solid #e5e7eb;">
      <td style="padding:0.75rem; font-variant-numeric:tabular-nums;">5</td>
      <td style="padding:0.75rem;"><strong>Pages, assignments, discussions, quizzes</strong></td>
      <td style="padding:0.75rem;">The actual content. Quiz questions come after their quiz.</td>
    </tr>
    <tr style="border-bottom:1px solid #e5e7eb;">
      <td style="padding:0.75rem; font-variant-numeric:tabular-nums;">6</td>
      <td style="padding:0.75rem;"><strong>Modules</strong></td>
      <td style="padding:0.75rem;">The containers students actually navigate.</td>
    </tr>
    <tr style="border-bottom:1px solid #e5e7eb;">
      <td style="padding:0.75rem; font-variant-numeric:tabular-nums;">7</td>
      <td style="padding:0.75rem;"><strong>Module items</strong></td>
      <td style="padding:0.75rem;">Needs every ID from step 5. This is where hand-rolled scripts break.</td>
    </tr>
    <tr style="border-bottom:1px solid #e5e7eb;">
      <td style="padding:0.75rem; font-variant-numeric:tabular-nums;">8</td>
      <td style="padding:0.75rem;"><strong>Front page and syllabus</strong></td>
      <td style="padding:0.75rem;">The front page has to exist as a page first.</td>
    </tr>
    <tr style="border-bottom:1px solid #e5e7eb;">
      <td style="padding:0.75rem; font-variant-numeric:tabular-nums;">9</td>
      <td style="padding:0.75rem;"><strong>Enrollments</strong></td>
      <td style="padding:0.75rem;">Real people. Added only once the course is worth looking at.</td>
    </tr>
    <tr style="background:#f0f9ff; border-bottom:1px solid #e5e7eb;">
      <td style="padding:0.75rem; font-variant-numeric:tabular-nums;">10</td>
      <td style="padding:0.75rem;"><strong>Publish</strong></td>
      <td style="padding:0.75rem;">Modules first, then the course — and the last step is a human's.</td>
    </tr>
  </tbody>
</table>

Steps 5 and 6 could run concurrently in principle. They don't. Canvas throttles on concurrency and serial execution is barely slower in practice.

## Publish last, and publish deliberately

Content objects, modules, module items, and the course each carry an **independent published flag.** Four levers, and getting any one of them wrong produces a course that is silently invisible.

Publishing a course while enrollments exist sends notification emails to real people. There is no satisfying undo for that — which is why the agent stops short of it.

It builds everything unpublished, publishes the modules once the structure is verified, and leaves the course itself for a person to turn on. When it finishes, it says so explicitly and gives you the one-line command to go live.

<a href="/images/updates/canvas-course-builder-agent-page.png" target="_blank" rel="noopener noreferrer"><img src="/images/updates/canvas-course-builder-agent-page.png" alt="The agent reporting that all modules are published while the course itself remains unpublished, with the publish command and a manifest path, beside the finished Canvas page for The AI Education Revolution showing styled callout cards." width="2826" height="1588" /></a>

That screenshot is the finished result on the right: a real Canvas page with a module heading, a two-column Challenge/Solution comparison, and a highlighted Key Insight block. The visual hierarchy came from the prompt asking for it.

## Run it twice, get one course

Canvas has no upsert. A second run of a naive script produces a **second copy of every page, assignment, and module** — and nobody notices until a student does.

The agent writes a **manifest**: a JSON file mapping each object in the spec to the ID Canvas assigned it. On a re-run, anything already in the manifest gets updated in place instead of created again.

That also makes a failed build resumable. If it dies halfway through step 7, you run the same command and it picks up from where it stopped rather than starting a duplicate course beside the broken one.

The manifest path is reported at the end of every build, so re-runs are predictable and you always know which state file governs which course.

## Keep talking to it after the build

The build isn't the end of the conversation. In the screenshot above, the follow-up is just: *"update the page on `The AI Education Revolution` and add more content. the content right now is too short."*

The agent finds the page, rewrites the body, and pushes it back — no rebuild, no duplicate. Reviewing a course and revising it are the same interface.

## Things it knows that cost hours to learn

A handful of Canvas behaviors return `200 OK` while quietly doing nothing. The agent is built to expect them:

- **Pages are referenced by slug, not by ID.** Every other content type wires into a module with `content_id`; pages use `page_url`. This is the single most common module-wiring failure.
- **Course dates need a flag to exist.** `start_at` and `end_at` are silently discarded unless the course is set to restrict enrollments to course dates. Canvas returns the course with null dates and no error.
- **A new course may not be empty.** If the account has a course template, Canvas copies it into every new course — so a build can land on top of someone else's content unless it asks for a clean shell.
- **Some create parameters are not nested.** Nest them the way the rest of the API works and they're accepted and ignored. The most visible symptom: the course simply doesn't publish.
- **Collections paginate at 10 by default,** through a `Link` header rather than a page count. Anything that reads existing objects has to follow it or it silently sees a fraction of the course.
- **Throttling looks like a permissions error.** Canvas returns `403` for rate limiting, distinguishable from a real permissions failure only by the body text. Sequential requests almost never hit it; parallel ones are the usual cause.

## Use a migration when a migration is the answer

If you're duplicating an existing course, copying between instances, or importing from another LMS, the **Content Migrations API does in one call** what would otherwise be hundreds — and it preserves internal links and dates, which hand-built copies do not.

The agent is built to recognize that case and say so, rather than hand-rolling something a migration would handle better.

For roster loading at scale, SIS imports beat per-user enrollment calls by orders of magnitude, but they can deactivate enrollments not present in the file. That one stays a deliberate human decision.

## All of this is a skill

Everything above — the build order, the publish discipline, the manifest, the Canvas quirks — lives in a single **Markdown skill file** attached to the agent.

<a href="/images/updates/canvas-course-builder-agent-skill.png" target="_blank" rel="noopener noreferrer"><img src="/images/updates/canvas-course-builder-agent-skill.png" alt="The Edit Skill dialog in the Agentic OS agent editor showing the canvas-course-builder skill, version 1.0.0, with its description field and the beginning of its Markdown instruction text." width="1344" height="1481" style="max-width:640px; display:block; margin:1.5rem auto;" /></a>

It's a named, versioned entry in the agent's Skills tab — `canvas-course-builder`, version `1.0.0` — with a description that tells the agent when to reach for it and an instruction body it reads at the point of use.

Which means the interesting part is editable by the people who own the process. If your institution has a house style for module naming, or a rule that quizzes are never auto-published, you write that sentence into the skill. No plugin, no deployment, no code review.

Skills are portable across agents, too. The same file can back a course-authoring agent, a curriculum-review agent, and an LMS-migration agent without being rewritten three times.

## Where to find it

The Course Authoring agent is available in Agentic OS. Skills are configured per agent under **Configurations → Skills**, and the same editor is where you'd add your own.

If you want to see what else agents on the platform can be given: the [ibl.ai agent catalog](https://ibl.ai/solutions/higher-education#agents) has around a hundred pre-built configurations across higher education, K-12, enterprise, government, and regulated verticals — each one a set of Markdown files you can read, edit, and own.
