nav4ai
Lesson materials and homework
- Category
- Pending
- Primary Subcategory
- Pending
Integration details
Description
nav4ai helps independent teachers review their students, groups, and completed homework in ChatGPT. Retrieve teaching context, including requested private notes and completed answers, then create, edit, style, and preview interactive lesson or homework materials. Import approved host-supplied images into materials. Changes to attached materials also update their lesson or homework content in nav4ai.
- Integration type
- Plugin
- Verification status
- Not applicable
- Platform
- ChatGPT
- Category
- Pending
- Primary Subcategory
- Pending
- Secondary Subcategories
- None listed
- Brand
- Unknown
- Access
- Account required
- First tracked
- 2026-09-23
- Tool count
- 15
- Geography
- US
The broad Category that contains the Primary Subcategory.
The Primary Subcategory used for this profile’s headline score.
Other Subcategories where the Integration is listed.
Get alerts for nav4ai
Get updates when nav4ai’s Discoverability Score or category rank changes.
ChatGPT Plugin Discovery Score
ChatGPT Plugin discovery is coming soon
ChatGPT can surface a Plugin when it matches a user's request.Your Plugin Discovery Score measures how often yours appears.
No spam. Unsubscribe any time.
What discovery looks like

Competitive lineup
15 tools agents can invoke
Create a blank lesson or homework material in the authenticated teacher's Materials root. This does not start AI generation, publish homework, attach to a learner, or delete anything. Build content with edit_material afterwards.
create_material
Edit one owned material's sections and blocks by running Python in the same typed constructor sandbox as nav4ai's in-app editor. The code runs against the current lesson, which `get_material` returns as `lesson` in exactly this constructor form; every id there is stable and must be reused when patching. Read nav4ai://guides/lesson-authoring before building substantial content. Nothing is validated while the code runs: at the end of the call every node is checked once, an invalid new node is dropped, an invalid edit reverts to the version the call started with, and a printed line names each one with the reason. The printed output comes back as `report` and is the record of the call; whatever it reports as dropped or reverted is not in the lesson. A syntax or runtime error persists nothing. Successful runs persist atomically as one revision and return the refreshed `lesson`; `changed` is false when the lesson is unchanged. Pass `expected_updated_at` from the last result to refuse edits on a stale revision. This tool never changes title, level, theme, or decorative visuals: use update_material_metadata and style_material. Audio and video need an already available supported URL. Images come from import_material_image and are passed as `image=` to the constructors that accept one (`cloze`, `anagram`, `word_bank`, flashcard `card(...)`) or as `video(poster=)`. External embeds are accepted only for Wordwall, Miro, or Quizlet links the teacher supplied in `teacher_links` or that are already in the lesson. Compact signatures: ```py # Constructors build drafts: `section(...)` for a section, `<block>(...)` for a block. The id comes # first and is unique and stable: `"s2"` for sections, `"<section>-<type>-<n>"` for blocks. # Every field is optional in the signature because one constructor serves two purposes: # - creating: pass the fields listed under "Required to create" in the docstring, then add the # draft with `add_sections(...)` / `add_blocks(...)`; # - patching: pass only the fields to change and apply the draft with `patch_node(...)`; unpassed # fields keep their value, semantic fields (lines, options, cards, buckets, ...) are replaced whole. # Omitted optional fields take the renderer's defaults. Every exercise also accepts `feedback=`, # `points=`, `maxAttempts=`, `checkLabel=`, `retryLabel=` as extra keywords. # Nothing is validated while building. At the end of the call every node is checked once: an invalid # new node is dropped, an invalid edit reverts to the version the call started with, and a line says # which and why. A constructor that cannot build at all prints one line and returns None, and None # drafts are skipped by add/patch. from typing import Any, Literal, TypeAlias, TypedDict Draft: TypeAlias = dict[str, Any] Node: TypeAlias = Draft SectionNode: TypeAlias = Draft Image: TypeAlias = str | dict[str, Any] class Feedback(TypedDict, total=False): correct: str incorrect: str class Answer(TypedDict): label: str correct: bool class Error(TypedDict): token: str correction: str class Gap(TypedDict): ... class Card(TypedDict): ... class VocabularyItem(TypedDict): ... # --- content blocks ----------------------------------------------------------- def section(id: str, title: str | None = None, blocks: list[Draft | None] | None = None, eyebrow: str | None = None, subtitle: str | None = None, headerPlacement: Literal["inside", "border", "band"] | None = None) -> Draft: """ A lesson section holding blocks. `eyebrow` is a one-to-three-word badge; a sentence goes in `subtitle`. Required to create: nothing. section("s1", "Warm-up", eyebrow="5 min", blocks=[paragraph("s1-paragraph-1", "Look at the picture."), quiz_mcq("s1-quiz-mcq-1", ...)]) """ def paragraph(id: str, text: str | None = None) -> Draft: """ One coherent chunk of prose; do not split every sentence into its own block. Required to create: text. paragraph("s1-paragraph-1", "The past simple describes finished actions.") """ def heading(id: str, *, text: str | None = None, level: Literal[1, 2, 3] | None = None, align: Literal["left", "center", "right"] | None = None) -> Draft: """ A standalone title inside a section. Required to create: text. heading("s2-heading-1", text="Grammar focus", level=3) """ def hero(id: str, *, title: str | None = None, eyebrow: str | None = None, subtitle: str | None = None, lead: str | None = None, align: Literal["left", "center", "right"] | None = None) -> Draft: """ The bold lesson opener; use once, at the top. Required to create: title. hero("s1-hero-1", title="Past Simple", eyebrow="Unit 3", subtitle="Talking about yesterday") """ def callout(id: str, *, body: str | None = None, variant: Literal["tip", "note", "warning", "grammar", "example"] | None = None, title: str | None = None) -> Draft: """ A highlighted box for a tip, note, warning, grammar point or example. Required to create: body. callout("s2-callout-1", body="Regular verbs take -ed.", variant="grammar", title="Rule") """ def objectives(id: str, *, items: list[str] | None = None, title: str | None = None) -> Draft: """ A "you will learn" checklist near the top of the lesson. Required to create: items. objectives("s1-objectives-1", items=["Form the past simple", "Talk about last weekend"]) """ def divider(id: str, label: str | None = None) -> Draft: """ A horizontal rule, optionally captioned. Required to create: nothing. divider("s3-divider-1", "Practice") """ def banner(id: str, *, title: str | None = None, subtitle: str | None = None, subtitlePill: bool | None = None, instruction: str | None = None, align: Literal["left", "center"] | None = None) -> Draft: """ A themed text-only title header. Banner art belongs in root visuals, not here. Required to create: title. banner("s1-banner-1", title="Lesson 4", subtitle="Food and drink", instruction="Read, then answer.") """ def vocabulary_item(translation: str | None = None, *, example: str | None = None, ipa: str | None = None) -> VocabularyItem: """ One entry of a vocabulary list; the term itself is the dict key in `vocabulary(items=...)`. vocabulary_item("кіт", example="The cat sleeps.", ipa="kæt") """ def vocabulary(id: str, *, items: dict[str, VocabularyItem | str] | None = None, title: str | None = None, language: str | None = None) -> Draft: """ A word list keyed by term. A bare string value is the translation. Required to create: items. vocabulary("s2-vocabulary-1", items={"cat": "кіт", "dog": vocabulary_item("пес", example="The dog barks.")}) """ def dialogue(id: str, *, lines: list[tuple[str, str] | str] | None = None, title: str | None = None) -> Draft: """ A speaker-attributed conversation; `(speaker, text)` tuples or `"Speaker: text"` strings. Required to create: lines. dialogue("s2-dialogue-1", lines=[("Anna", "Hi! How was your weekend?"), "Ben: Great, I went hiking."]) """ def audio(id: str, *, src: str | None = None, title: str | None = None, transcript: list[str] | None = None) -> Draft: """ A listening clip. `src` is copied from the brief or the lesson, never invented; without one, leave the block out and say so. Required to create: src. audio("s3-audio-1", src="https://.../track1.mp3", title="Listening 1", transcript=["Hello.", "Hi there."]) """ def video(id: str, *, src: str | None = None, poster: str | None = None, title: str | None = None) -> Draft: """ A YouTube/Vimeo watch URL or a direct video file, copied from the brief or the lesson. Required to create: src. video("s3-video-1", src="https://youtu.be/abc123", title="Watch: ordering food") """ # --- exercise pieces ----------------------------------------------------------- def answer(label: str) -> Answer: """ Marks an option as correct inside `quiz_mcq`, `odd_one_out` and `select_gap`. quiz_mcq("s3-quiz-mcq-1", prompt="Pick the past form.", options=["go", answer("went"), "goed"]) """ def error(token: str, correction: str) -> Error: """ Marks the one wrong token of an `error_correction` sentence and gives its fix. error_correction("s3-error-correction-1", tokens=["I", error("goed", "went"), "home"]) """ def gap(*answers: str, caseSensitive: bool = False, hints: list[str] | None = None) -> Gap: """ A blank the student types into; every listed answer is accepted. cloze("s3-cloze-1", ["She ", gap("has", "'s"), " already left."]) """ def select_gap(options: list[str | Answer], *, hints: list[str] | None = None) -> Gap: """ A blank picked from a dropdown; exactly one option is wrapped in `answer(...)`. Needs at least 2 options. cloze("s3-cloze-2", ["I saw ", select_gap([answer("a"), "an", "the"]), " cat."]) """ def card(back: str, *, hint: str | None = None, image: Image | None = None) -> Card: """ The back of a flashcard; the front is the dict key in `flashcards(cards=...)`. flashcards("s2-flashcards-1", cards={"cat": card("кіт", hint="animal")}) """ # --- exercises ----------------------------------------------------------------- def quiz_mcq(id: str, *, prompt: str | None = None, options: list[str | Answer] | None = None) -> Draft: """ One multiple-choice question. Wrap correct options in `answer(...)`; several marked options make it multi-select. Required to create: prompt, options (with one answer(...)). quiz_mcq("s3-quiz-mcq-1", prompt="Which are past forms?", options=[answer("went"), "go", answer("saw"), "see"]) """ def cloze(id: str, items: list[str | Gap] | None = None, *, image: Image | None = None) -> Draft: """ A gap-fill passage: plain strings interleaved with `gap(...)`/`select_gap(...)`. No `text`, no `_` markers. Required to create: items (with at least one gap). cloze("s3-cloze-1", ["She ", gap("has"), " left and he ", select_gap([answer("has"), "have"]), " stayed."]) """ def flashcards(id: str, *, cards: dict[str, str | Card] | None = None, shuffle: bool | None = None, summary: str | None = None, frontLabel: str | None = None, backLabel: str | None = None, flipLabel: str | None = None, knownLabel: str | None = None, unknownLabel: str | None = None, restartLabel: str | None = None) -> Draft: """ A flip-card study deck keyed by front. A bare string value is the back. Required to create: cards. flashcards("s2-flashcards-1", cards={"cat": "кіт", "dog": card("пес", hint="animal")}, shuffle=True) """ def match(id: str, *, pairs: dict[str, str] | None = None, prompt: str | None = None, leftLabel: str | None = None, rightLabel: str | None = None) -> Draft: """ Two-column matching, left to right. At least 2 pairs. Required to create: pairs. match("s3-match-1", pairs={"go": "went", "see": "saw", "eat": "ate"}, prompt="Match the verb to its past form.") """ def truefalse(id: str, *, text: str | None = None, answer: bool | None = None, prompt: str | None = None, trueLabel: str | None = None, falseLabel: str | None = None) -> Draft: """ One statement with a True/False toggle. One statement = one block; add siblings for more. Required to create: text, answer. truefalse("s3-truefalse-1", text="Cats can fly.", answer=False) """ def order(id: str, *, parts: list[str] | None = None, prompt: str | None = None, shuffle: bool | None = None) -> Draft: """ Arrange parts into the correct sequence. At least 2 parts, given in the right order. Required to create: parts. order("s3-order-1", parts=["I", "have", "finished"], prompt="Make a sentence.") """ def odd_one_out(id: str, *, items: list[str | Answer] | None = None, prompt: str | None = None, reason: str | None = None) -> Draft: """ Tap the item that does not belong; exactly one is wrapped in `answer(...)`. Required to create: items (with one answer(...)). odd_one_out("s3-odd-one-out-1", items=["apple", "pear", answer("carrot")], reason="A carrot is a vegetable.") """ def tfng(id: str, *, text: str | None = None, answer: Literal["true", "false", "not-given"] | None = None, passage: str | None = None, prompt: str | None = None, trueLabel: str | None = None, falseLabel: str | None = None, notGivenLabel: str | None = None) -> Draft: """ Reading statement judged True / False / Not Given against `passage`. Required to create: text, answer. tfng("s3-tfng-1", text="The author lives in Kyiv.", answer="not-given", passage="...") """ def verb_conjugation(id: str, *, subject: str | None = None, verb: str | None = None, accepted: list[str] | None = None, tense: str | None = None, prompt: str | None = None, caseSensitive: bool | None = None) -> Draft: """ Type the conjugated form for one subject + verb cue. Required to create: subject, verb, accepted. verb_conjugation("s3-verb-conjugation-1", subject="she", verb="go", tense="past simple", accepted=["went"]) """ def word_formation(id: str, items: list[str | Gap] | None = None, *, root: str | None = None, prompt: str | None = None) -> Draft: """ One sentence with exactly one `gap(...)` that the student fills by transforming `root`. Required to create: items (with exactly one gap), root. word_formation("s3-word-formation-1", ["She is very ", gap("happy"), " today."], root="HAPPINESS") """ def anagram(id: str, *, answer: str | None = None, scrambled: str | None = None, clue: str | None = None, image: Image | None = None, prompt: str | None = None, caseSensitive: bool | None = None) -> Draft: """ Unscramble letters into one word; omit `scrambled` to auto-shuffle. Required to create: answer. anagram("s3-anagram-1", answer="banana", clue="A yellow fruit") """ def word_bank(id: str, *, tokens: list[str] | None = None, prompt: str | None = None, image: Image | None = None, distractors: list[str] | None = None, acceptedAnswers: list[list[str]] | None = None, caseSensitive: bool | None = None) -> Draft: """ Build a sentence from shuffled word tiles; always add distractors. Required to create: tokens. word_bank("s3-word-bank-1", tokens=["I", "went", "home"], distractors=["go", "goed"]) """ def error_correction(id: str, *, tokens: list[str | Error] | None = None, options: list[str] | None = None, prompt: str | None = None, caseSensitive: bool | None = None) -> Draft: """ A sentence split into tokens with exactly one `error(token, correction)`. Required to create: tokens (with one error(...)). error_correction("s3-error-correction-1", tokens=["She", error("don't", "doesn't"), "like", "tea"]) """ def free_text_input(id: str, *, prompt: str | None = None, placeholder: str | None = None, rows: int | None = None, limit: int | None = None, limitUnit: Literal["words", "chars"] | None = None, submitLabel: str | None = None) -> Draft: """ An open written answer, saved for the teacher; not graded. Required to create: nothing. free_text_input("s4-free-text-input-1", prompt="Describe your last weekend.", limit=80) """ def voice_response(id: str, *, prompt: str | None = None, modelAudio: dict[str, Any] | None = None, showTranscript: bool | None = None, maxDuration: Literal[30, 60, 120, 300] | None = None) -> Draft: """ An ungraded spoken answer. Model audio is added by the teacher later. Required to create: prompt. voice_response("s4-voice-response-1", prompt="Say three things you did yesterday.", maxDuration=60) """ def hangman(id: str, *, word: str | None = None, clue: str | None = None, prompt: str | None = None, lives: int | None = None) -> Draft: """ Guess a hidden word letter by letter; auto-checks. Required to create: word. hangman("s3-hangman-1", word="umbrella", clue="You need it in the rain.") """ def categorize(id: str, *, buckets: dict[str, list[str]] | None = None, prompt: str | None = None, shuffle: bool | None = None) -> Draft: """ Sort items into buckets, keyed by bucket label. At least 2 buckets and 2 items in total. Required to create: buckets. categorize("s3-categorize-1", buckets={"Nouns": ["book", "pen"], "Verbs": ["read", "write"]}) """ # --- exercise settings ---------------------------------------------------------- def add_feedback(draft: Draft | None, *, correct: str | None = None, incorrect: str | None = None) -> Draft | None: """ Attach feedback text to an exercise draft and return it. add_feedback(quiz_mcq(...), correct="Well done!", incorrect="Look at the time marker.") """ def add_grading(draft: Draft | None, *, points: float | None = None, maxAttempts: int | None = None, checkLabel: str | None = None, retryLabel: str | None = None) -> Draft | None: """ Attach scoring to an exercise draft and return it. `points=0` is dropped. add_grading(match(...), points=2, maxAttempts=3) """ # --- editing the lesson ----------------------------------------------------------- def get_sections() -> list[SectionNode]: """ Return the current lesson as a list of sections. """ def find_section(id: str) -> SectionNode | None: """ The section with that id, or None (a line is printed). section = find_section("s2") """ def find_node(id: str) -> tuple[SectionNode, Node] | None: """ A block and the section that holds it. parent, block = find_node("s2-paragraph-1") """ def section_nodes(section: SectionNode) -> list[Node]: """ The blocks of a section, in order. for block in section_nodes(find_section("s1")): ... """ def add_sections(new: list[Draft | None], *, after_id: str | None = None, before_id: str | None = None) -> None: """ Insert sections after/before a section id, or append. add_sections([section("s4", "Homework", blocks=[free_text_input("s4-free-text-input-1", prompt="Write 5 sentences.")])], after_id="s3") """ def add_blocks(new: list[Draft | None], section_id: str | None = None, *, after_id: str | None = None, before_id: str | None = None) -> None: """ Insert blocks into `section_id` (appends), or next to a block id. add_blocks([truefalse("s2-truefalse-1", text="Cats bark.", answer=False)], after_id="s2-quiz-mcq-1") add_blocks([paragraph("s1-paragraph-2", "Read the text.")], "s1") """ def patch_node(draft: Draft | None) -> Draft | None: """ Apply a draft to the existing block or section with the same id and type; only the passed fields change. patch_node(heading("s2-heading-1", text="Grammar focus", level=3)) patch_node(dialogue("s4-dialogue-1", lines=[("A", "Hi!"), "B: Hello."])) patch_node(section("s1", title="Warm-up")) """ def replace_node(id: str, new: Draft | None) -> Draft | None: """ Swap a block for a whole new draft, keeping the old id; use it to change a block's type. replace_node("s2-quiz-mcq-1", truefalse("s2-quiz-mcq-1", text="Cats bark.", answer=False)) """ def remove_nodes(ids: list[str]) -> None: """ Remove sections and/or blocks by id. remove_nodes(["s3", "s2-divider-1"]) """ def move_sections(ids: list[str], *, after_id: str | None = None, before_id: str | None = None) -> None: """ Move sections, in the given order, next to another section. move_sections(["s4"], before_id="s2") """ def move_blocks(ids: list[str], section_id: str | None = None, *, after_id: str | None = None, before_id: str | None = None) -> None: """ Move blocks into `section_id` (appends) or next to a block id, across sections. move_blocks(["s1-callout-1"], after_id="s2-paragraph-1") """ async def external_embed(url: str, title: str | None = None) -> Draft | None: """ A Wordwall, Miro or Quizlet activity from a link the teacher supplied, as given. Must be awaited. add_blocks([await external_embed("https://wordwall.net/resource/12345")], "s3") """ ``` The signatures above are reference-only and must never be output. Return only executable lesson-edit statements. Never reproduce imports, type aliases, TypedDicts, classes, def/async def declarations, ellipsis stubs, Ruff/fmt directives, comments, or Markdown fences—even when correcting old code.
edit_material
Get one owned group's name, level, teacher-private note, archive state, and active member references. Does not return billing, photos, or meeting links.
get_group_context
Get a completed submission's effective score, teacher comment, and answer attempts. Returns no drafts, capability tokens, voice media URLs, database attempt IDs, or debug fields.
get_homework_submission
Get one owned lesson or homework material for editing: metadata, the lesson as the constructor code edit_material accepts (every id is stable and must be reused when patching), and the current theme. Use show_material to render a preview for the teacher.
get_material
Get teaching context for one student owned by the authenticated teacher: display name, status, level, goal, teacher-private notes, locale, and timezone. Does not return email, billing, photos, or meeting links.
get_student_context
Import one image supplied by the MCP host into nav4ai storage for an owned material. The temporary source URL and host file reference are never persisted or returned. This tool does not change the material; after import, use edit_material to pass the returned stable URL as `image=` to a constructor that accepts one (`cloze`, `anagram`, `word_bank`, flashcard `card(...)`) or as `video(poster=)`.
import_material_image
List teaching groups owned by the authenticated teacher without billing or meeting details.
list_groups
List active homework owned by the authenticated teacher with its learner/group target and aggregate submission counts. Capability links and homework content are never returned.
list_homework
List completed submissions for one owned homework assignment. Draft work, capability tokens, answers, and voice media are omitted; use get_homework_submission for completed answer history.
list_homework_submissions
List lesson and homework materials owned by the authenticated teacher. Returns concise metadata without content, conversations, learner links, or internal workflow fields.
list_materials
List students owned by the authenticated teacher. Returns teaching references, display names, statuses, and levels; use get_student_context for goals or private notes.
list_students
Render an interactive preview of one owned material for the teacher and return its complete lessons-sdk v4 manifest. Read-only: call it when the teacher wants to see the material, typically once after a round of edits or styling rather than after every call.
show_material
Change one owned material's theme: background, width, density, palette, fonts, progress, finish, and scoped CSS. Read nav4ai://guides/lesson-styling first and finish the teaching content before styling. `theme` is a patch merged into the current theme (returned by get_material as `theme`) top-level field by field: send only the fields that change; an omitted field keeps its value and a null is ignored, never applied. A field you do send replaces the current one whole, including the nested `palette`, `fontPair`, `progress`, and `finish` objects, so to change one palette role send the whole palette. `css` is never partial: it replaces the complete stylesheet, so a one-rule change resends every current rule plus the changed one. Colours are solid hex strings and `fonts` lists the Google Fonts families the CSS and `fontPair` use. Unknown keys are dropped. The change persists atomically and returns the refreshed theme; `changed` is false when nothing differed.
style_material
Update one owned material's title, level, or lesson/homework kind. Title and level are synchronized into the manifest envelope. This tool cannot otherwise change lesson content or theme.
update_material_metadata
How do I improve a ChatGPT Plugin's discoverability?
The levers are the listing surface agents actually read: names, descriptions, keywords, tool metadata, and registry health. Which lever matters depends on where discovery breaks, which is what continuous measurement shows.
Where is this profile measured?
This profile uses the geography attached to the latest public registry snapshot: US. Locale tags are intentionally omitted.