Synchronize local course content with Canvas.
Install CanvasSync in the local virtual environment:
source .venv/bin/activate
pip install -e .Create a starter config:
cp config.yaml.sample config.yamlEdit config.yaml and set at least:
api_urlcourse_sismodules
If you do not know the course_sis value, see Finding the course SIS ID.
Then pass your Canvas API access token on the command line. If you do not have one yet, see Canvas API access token.
export CANVAS_API_KEY="your-canvas-api-token"
canvas-sync --config config.yaml --api-key "$CANVAS_API_KEY" coursesWhen the course lookup works, run a limited sync first:
canvas-sync --config config.yaml --api-key "$CANVAS_API_KEY" sync --limit "Week 1"Then run the full sync:
canvas-sync --config config.yaml --api-key "$CANVAS_API_KEY" syncGlobal options must come before the subcommand:
canvas-sync --config config.yaml --api-key "$CANVAS_API_KEY" syncCommon commands:
courses: list courses visible to the Canvas API token.sync: synchronize the configured syllabus, modules, pages, files, URLs, and assignment metadata.sync --limit TEXT: synchronize only modules whose names matchTEXT. Repeat--limitto match more than one module.render PATH: render one Markdown file to HTML without updating Canvas pages or modules. Local images may still be uploaded during rendering.render PATH --output rendered.html: render Markdown to an output file.roster: print the course roster as a table.roster --section TEXT: print students from matching sections only.roster --email: print roster entries as email addresses.dump: print the current Canvas module and item outline for inspection.
sync changes Canvas content. It can create, update, reorder, publish, unpublish, and delete module items to make Canvas match config.yaml.
Note the following:
- Module and item order in
config.yamlmatter and are reflected in Canvas. - Extra Canvas module items after the configured items may be deleted.
- Assignments referenced with
assignment:must already exist in Canvas. - Local files referenced with
file:,page:,description:, orsyllabus:are resolved relative to the config file directory unless--rootis provided. - Run
sync --limit TEXTfirst when testing changes to one module.
CanvasSync needs a Canvas API access token for the api_key value.
To create one:
- Sign in to Canvas in your browser.
- In Global Navigation, open Account, then Settings.
- Find the Approved Integrations section.
- Click Add New Access Token.
- Enter a purpose such as
CanvasSync. - Choose an expiration date if appropriate.
- Click Generate Token.
- Copy the generated token before closing the dialog.
Treat this token like a password. It can access Canvas with your account's permissions. If the token is exposed, delete or regenerate it in Canvas. If the Add New Access Token button is disabled, your institution may require an administrator to manage access tokens.
See Instructure's guide: How do I manage API access tokens in my user account?
Prefer passing the token with --api-key instead of writing it into config.yaml:
canvas-sync --config config.yaml --api-key "$CANVAS_API_KEY" coursesDo not commit real Canvas tokens. Revoke any token that is accidentally exposed.
CanvasSync reads course configuration from a YAML file passed with --config. Use config.yaml.sample as a starting point:
cp config.yaml.sample config.yamlMinimal example:
api_url: "https://drexel.instructure.com"
course_sis: "SIS EXAMPLE 202535"
syllabus: syllabus/syllabus.md
modules:
- name: "Week 1"
published: true
items:
- title: "Welcome"
published: true
page: pages/welcome.md
- title: "Lecture Slides"
published: true
file: files/week-01-slides.pdf
- title: "Assignment 1"
published: true
assignment: "Assignment 1"
description: assignments/assignment-01.md
points_possible: 100
due_at: "1/19/2026 11:59pm"Top-level keys:
api_url: Canvas site URL, such ashttps://canvas.instructure.com.api_key: Canvas API access token. You can omit this when passing--api-keyon the command line.course_sis: Canvas SIS course ID to synchronize. This value may contain spaces, so quote it in YAML.pandoc_metadata: optional metadata passed to Pandoc when rendering Markdown.vars: optional global variables promoted directly into Jinja templates. Usevarsfor short, frequently used values that you want to reference by name, such as{{ instructor_name }}.data: optional structured data available in Jinja templates assite. Usedatafor grouped course or site information, such as{{ site.course.title }}, to avoid cluttering the top-level template namespace.syllabus: optional Markdown file used to update the Canvas course syllabus.modules: ordered list of Canvas modules to create or synchronize.
Each module supports:
name: module name.published: whether the module is published. Defaults tofalse.unlock_at: optional date/time string.items: ordered list of module items.
Each item needs a title and one content key. Nested items become indented module items.
page: render a Markdown file and create or update a Canvas page.page_contents: render inline Markdown text and create or update a Canvas page.file: upload a local file and add it to the module.url: add an external URL. Usenew_tab: trueto open it in a new tab.assignment: link an existing Canvas assignment. Optional fields includedescription,description_contents,points_possible,unlock_at,due_at, andlock_at.vars: item-specific template variables. These are available when rendering that item'spage,page_contents, assignmentdescription, or assignmentdescription_contents, and override top-levelvarswith the same name.- No content key: create a Canvas module text header.
Paths are resolved relative to the config file directory unless --root is provided.
To find a course SIS ID in Canvas, open the course in your browser and go to Settings. On the Course Details tab, look for SIS ID. Copy that value exactly, including any spaces:
course_sis: "SIS EXAMPLE 202535"CanvasSync renders Markdown before sending content to Canvas:
- Jinja templates are expanded first.
- Markdown is converted to HTML with Pandoc.
- Bundled Lua filters are applied.
- A co-located
.cssfile is inlined when present. - Local images referenced by Markdown are uploaded to Canvas and rewritten to Canvas file preview URLs.
Values under vars are promoted directly into the template namespace. Values under data stay grouped under site.
Values under pandoc_metadata are passed to Pandoc as document metadata when Markdown is converted to HTML:
pandoc_metadata:
section: f2f
term: "Spring 2026"This is useful when Markdown, templates, or Pandoc filters need metadata that is not meant to be rendered directly as normal page content.
Bundled Lua filters are loaded automatically from src/canvassync/filters. Currently, conditional.lua supports section-specific content. It reads the Pandoc metadata value section and filters fenced Div blocks whose class starts with only-.
For example, with:
pandoc_metadata:
section: f2fthis Markdown keeps the only-f2f block and removes the only-online block:
::: {.only-f2f}
This appears only for the face-to-face section.
:::
::: {.only-online}
This appears only for the online section.
:::Any class matching only-VALUE is kept only when pandoc_metadata.section is VALUE.
Use top-level vars for short values that are convenient to reference directly throughout Markdown files:
vars:
instructor_name: "Ada Lovelace"
office_hours: "Tuesdays 2-4pm"Then use them by name in Markdown:
Instructor: {{ instructor_name }}
Office hours: {{ office_hours }}Use data for structured course data that should stay grouped under site:
data:
course:
title: "Example Course"
term: "Spring 2026"
links:
gradescope: "https://www.gradescope.com"Then reference it through site:
# {{ site.course.title }}
Term: {{ site.course.term }}
[Gradescope]({{ site.links.gradescope }})Module items can also define vars for values that only apply to that item:
modules:
- name: "Week 1"
items:
- title: "Assignment 1"
assignment: "Assignment 1"
description: assignments/assignment-01.md
vars:
github_classroom_link: "https://classroom.github.com/example"Those item-level variables are available while rendering assignments/assignment-01.md and take precedence over top-level variables with the same name.
This project uses pre-commit to run local quality checks before each commit.
Install the development dependencies and Git hooks:
source .venv/bin/activate
pip install -e ".[dev]"
pre-commit installRun all configured hooks manually:
pre-commit run -aThe current pre-commit configuration runs:
ruff-checkfor lintingdocformatterfor docstring formatting checksmypywith the configuration inpyproject.toml
You can also run an individual hook:
pre-commit run mypy -a
pre-commit run docformatter -a
pre-commit run ruff-check -a