By Sagar Shankaran, Founder of CallSphere
Build an AI course creation agent that generates curriculum outlines mapped to learning objectives, drafts lesson content, and designs aligned assessments — all from a topic description.
Key takeaways
Building a course is a complex, multi-stage process that most educators dread. You need learning objectives that are measurable, a logical content sequence, lessons that build on each other, and assessments that actually test what was taught. An AI course creation agent automates this pipeline while maintaining pedagogical rigor by following established instructional design frameworks like Bloom's Taxonomy and backward design.
The pipeline has four stages: Objective Mapping, Outline Generation, Content Drafting, and Assessment Design. Each stage's output feeds into the next, creating a coherent course where every lesson and quiz ties back to specific learning objectives.
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
class BloomLevel(str, Enum):
REMEMBER = "remember"
UNDERSTAND = "understand"
APPLY = "apply"
ANALYZE = "analyze"
EVALUATE = "evaluate"
CREATE = "create"
@dataclass
class LearningObjective:
objective_id: str
description: str
bloom_level: BloomLevel
measurable_verb: str # e.g., "identify", "compare", "design"
assessment_criteria: str
@dataclass
class Lesson:
lesson_id: str
title: str
module: str
order: int
objectives: list[str] # objective_ids
estimated_duration_minutes: int
content: str = ""
prerequisites: list[str] = field(default_factory=list)
key_concepts: list[str] = field(default_factory=list)
@dataclass
class QuizItem:
question: str
question_type: str
correct_answer: str
distractors: list[str] = field(default_factory=list)
objective_id: str = "" # Links back to learning objective
bloom_level: str = ""
explanation: str = ""
@dataclass
class Module:
module_id: str
title: str
description: str
order: int
lessons: list[Lesson] = field(default_factory=list)
quiz: list[QuizItem] = field(default_factory=list)
@dataclass
class CourseOutline:
title: str
description: str
target_audience: str
prerequisites: list[str]
objectives: list[LearningObjective] = field(default_factory=list)
modules: list[Module] = field(default_factory=list)
total_hours: float = 0.0
The backward design approach starts with what students should be able to do after completing the course, then works backward to content:
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
flowchart LR
CALLER(["Student or Parent"])
subgraph TEL["Telephony"]
SIP["Twilio SIP and PSTN"]
end
subgraph BRAIN["Education AI Agent"]
STT["Streaming STT<br/>Deepgram or Whisper"]
NLU{"Intent and<br/>Entity Extraction"}
TOOLS["Tool Calls"]
TTS["Streaming TTS<br/>ElevenLabs or Rime"]
end
subgraph DATA["Live Data Plane"]
CRM[("CRM and Notes")]
CAL[("Calendar and<br/>Schedule")]
KB[("Knowledge Base<br/>and Policies")]
end
subgraph OUT["Outcomes"]
O1(["Enrollment captured"])
O2(["Tour scheduled"])
O3(["Counselor callback"])
end
CALLER --> SIP --> STT --> NLU
NLU -->|Lookup| TOOLS
TOOLS <--> CRM
TOOLS <--> CAL
TOOLS <--> KB
NLU --> TTS --> SIP --> CALLER
NLU -->|Resolved| O1
NLU -->|Schedule| O2
NLU -->|Escalate| O3
style CALLER fill:#f1f5f9,stroke:#64748b,color:#0f172a
style NLU fill:#4f46e5,stroke:#4338ca,color:#fff
style O1 fill:#059669,stroke:#047857,color:#fff
style O2 fill:#0ea5e9,stroke:#0369a1,color:#fff
style O3 fill:#f59e0b,stroke:#d97706,color:#1f2937
from agents import Agent, Runner
from pydantic import BaseModel
class ObjectiveOutput(BaseModel):
objectives: list[dict]
prerequisite_knowledge: list[str]
target_audience_description: str
objective_designer = Agent(
name="Learning Objective Designer",
instructions="""You design measurable learning objectives using
Bloom's Taxonomy. Given a course topic and target audience:
1. Generate 8-15 learning objectives that span Bloom's levels:
- 20% Remember/Understand (foundation)
- 40% Apply/Analyze (core skills)
- 40% Evaluate/Create (advanced skills)
2. Each objective MUST:
- Start with a measurable action verb from Bloom's Taxonomy
- Be specific enough to assess (not "understand databases" but
"design a normalized schema for a given business domain")
- Include the condition and criteria for success
3. Organize objectives in a logical learning sequence where each
builds on previous ones.
BLOOM'S VERBS BY LEVEL:
- Remember: list, define, identify, recall, name
- Understand: explain, summarize, classify, compare
- Apply: implement, execute, solve, demonstrate
- Analyze: differentiate, examine, deconstruct, debug
- Evaluate: justify, critique, assess, defend
- Create: design, construct, produce, compose
Return a JSON object with objectives, prerequisites, and
target audience description.""",
output_type=ObjectiveOutput,
)
async def generate_objectives(
topic: str, audience: str, duration_hours: float
) -> list[LearningObjective]:
result = await Runner.run(
objective_designer,
f"Topic: {topic}\nAudience: {audience}\n"
f"Course duration: {duration_hours} hours",
)
output = result.final_output_as(ObjectiveOutput)
objectives = []
for i, obj in enumerate(output.objectives):
objectives.append(LearningObjective(
objective_id=f"obj-{i+1:03d}",
description=obj["description"],
bloom_level=BloomLevel(obj["bloom_level"]),
measurable_verb=obj["verb"],
assessment_criteria=obj["criteria"],
))
return objectives
With objectives defined, generate a modular course outline that maps every objective to specific lessons:
class OutlineOutput(BaseModel):
modules: list[dict]
objective_coverage: dict # objective_id -> list of lesson_ids
outline_generator = Agent(
name="Course Outline Generator",
instructions="""You create course outlines that ensure complete
coverage of learning objectives. Given a set of objectives:
1. Group related objectives into modules (3-7 modules typical)
2. Within each module, create lessons (2-5 per module)
3. Each lesson should:
- Address 1-3 specific learning objectives
- Build on previous lessons (define prerequisites)
- Include estimated duration (30-90 minutes each)
- List key concepts to cover
4. COVERAGE CHECK: Every learning objective must appear in at least
one lesson. Verify this explicitly.
5. SEQUENCING RULES:
- Remember/Understand objectives before Apply objectives
- Concrete before abstract
- Simple before complex
- Each module should end with an integrative lesson
Return modules with their lessons and an objective coverage map.""",
output_type=OutlineOutput,
)
async def generate_outline(
topic: str,
objectives: list[LearningObjective],
duration_hours: float,
) -> list[Module]:
obj_text = "\n".join(
f"- [{o.objective_id}] ({o.bloom_level.value}) {o.description}"
for o in objectives
)
result = await Runner.run(
outline_generator,
f"Topic: {topic}\nDuration: {duration_hours}h\n"
f"Objectives:\n{obj_text}",
)
output = result.final_output_as(OutlineOutput)
modules = []
for i, mod in enumerate(output.modules):
lessons = []
for j, les in enumerate(mod["lessons"]):
lessons.append(Lesson(
lesson_id=f"les-{i+1}-{j+1}",
title=les["title"],
module=mod["title"],
order=j + 1,
objectives=les.get("objective_ids", []),
estimated_duration_minutes=les.get("duration", 45),
prerequisites=les.get("prerequisites", []),
key_concepts=les.get("key_concepts", []),
))
modules.append(Module(
module_id=f"mod-{i+1:02d}",
title=mod["title"],
description=mod["description"],
order=i + 1,
lessons=lessons,
))
return modules
With the structure defined, draft lesson content that covers the mapped objectives:
content_drafter = Agent(
name="Lesson Content Drafter",
instructions="""You draft educational lesson content. For each
lesson:
1. Start with a brief MOTIVATION section (why this matters)
2. Present CORE CONTENT with clear explanations
3. Include WORKED EXAMPLES that demonstrate the concept
4. Add PRACTICE EXERCISES (2-3 per lesson)
5. End with a KEY TAKEAWAYS summary
CONTENT GUIDELINES:
- Define every technical term on first use
- Use analogies to connect new concepts to familiar ones
- Include code examples, diagrams, or formulas where appropriate
- Mark prerequisite knowledge clearly
- Keep paragraphs short (3-5 sentences max)
- Use headers and bullet points for scannability
Each piece of content must demonstrably address its mapped
learning objectives. At the end, add a self-check: "After this
lesson, you should be able to: [restate objectives]" """,
)
async def draft_lesson_content(
lesson: Lesson,
objectives: list[LearningObjective],
previous_lessons: list[str],
) -> str:
mapped_objectives = [
o for o in objectives if o.objective_id in lesson.objectives
]
obj_text = "\n".join(
f"- {o.description} (Bloom: {o.bloom_level.value})"
for o in mapped_objectives
)
result = await Runner.run(
content_drafter,
f"Lesson: {lesson.title}\n"
f"Key concepts: {', '.join(lesson.key_concepts)}\n"
f"Duration: {lesson.estimated_duration_minutes} minutes\n"
f"Learning objectives to address:\n{obj_text}\n"
f"Prerequisites covered in previous lessons: "
f"{', '.join(previous_lessons)}",
)
return result.final_output
Generate quizzes that are aligned to specific learning objectives, ensuring every objective is assessed:
class AssessmentOutput(BaseModel):
questions: list[dict]
objective_coverage: dict
assessment_designer = Agent(
name="Assessment Designer",
instructions="""Design quiz questions aligned to learning objectives.
ALIGNMENT RULES:
1. Each question must map to exactly one learning objective
2. The question's cognitive demand must match the objective's
Bloom's level (a "remember" objective gets recall questions,
an "analyze" objective gets analysis questions)
3. Every objective must be assessed by at least one question
QUESTION DESIGN:
- Multiple choice: 4 options, one correct, three plausible distractors
- Short answer: clear rubric for what constitutes a correct answer
- Coding/practical: specific input/output expectations
DISTRACTOR QUALITY:
- Each distractor targets a specific misconception
- Distractors are the same length and format as the correct answer
- Avoid "all of the above" and "none of the above"
Return questions with objective mappings and an explicit coverage
check showing which objectives are assessed.""",
output_type=AssessmentOutput,
)
async def design_module_assessment(
module: Module,
objectives: list[LearningObjective],
questions_per_objective: int = 2,
) -> list[QuizItem]:
module_obj_ids = set()
for lesson in module.lessons:
module_obj_ids.update(lesson.objectives)
module_objectives = [
o for o in objectives if o.objective_id in module_obj_ids
]
obj_text = "\n".join(
f"- [{o.objective_id}] ({o.bloom_level.value}) "
f"{o.description} — Assess: {o.assessment_criteria}"
for o in module_objectives
)
result = await Runner.run(
assessment_designer,
f"Module: {module.title}\n"
f"Objectives to assess:\n{obj_text}\n"
f"Generate {questions_per_objective} questions per objective.",
)
output = result.final_output_as(AssessmentOutput)
items = []
for q in output.questions:
items.append(QuizItem(
question=q["question"],
question_type=q["type"],
correct_answer=q["correct_answer"],
distractors=q.get("distractors", []),
objective_id=q["objective_id"],
bloom_level=q.get("bloom_level", ""),
explanation=q.get("explanation", ""),
))
return items
import asyncio
async def create_course(
topic: str, audience: str, duration_hours: float
) -> CourseOutline:
# Stage 1: Learning objectives
objectives = await generate_objectives(topic, audience, duration_hours)
print(f"Generated {len(objectives)} learning objectives")
# Stage 2: Course outline
modules = await generate_outline(topic, objectives, duration_hours)
print(f"Created {len(modules)} modules")
# Stage 3: Draft content for each lesson
previous = []
for module in modules:
for lesson in module.lessons:
lesson.content = await draft_lesson_content(
lesson, objectives, previous
)
previous.append(lesson.title)
print(f"Drafted content for module: {module.title}")
# Stage 4: Design assessments per module
for module in modules:
module.quiz = await design_module_assessment(module, objectives)
print(f"Designed {len(module.quiz)} quiz items for {module.title}")
course = CourseOutline(
title=f"Complete Guide to {topic}",
description=f"A {duration_hours}-hour course on {topic}",
target_audience=audience,
prerequisites=[],
objectives=objectives,
modules=modules,
total_hours=duration_hours,
)
return course
# Usage
course = asyncio.run(
create_course("Python Web Development", "junior developers", 20)
)
The backward design approach enforces alignment at every stage. Each quiz question is tagged with a specific objective ID, and the assessment designer is required to produce a coverage map showing which objectives are tested. A validation step checks that every objective has at least one question and that the question's Bloom level matches the objective's Bloom level. For instance, a "create" objective cannot be assessed with a multiple-choice recall question — it requires a practical or project-based assessment.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
Yes. Because objectives, modules, lessons, and quizzes are linked by IDs, adding a new topic means generating new objectives, inserting lessons into the appropriate module, and designing additional quiz items. The outline generator can be re-run in "update" mode where it receives the existing outline and adds new elements without restructuring what already works. Existing content and assessments remain stable.
The content drafter receives a list of previous lesson titles and their key concepts as context. This prevents re-explaining concepts that were covered in earlier lessons and ensures consistent terminology. For additional consistency, you can add a "style guide" to the content drafter's system prompt that specifies voice, formatting conventions, code style, and terminology preferences. Running a final review pass with a separate editor agent that checks for contradictions across lessons adds another layer of quality control.
#CourseCreation #CurriculumDesign #EducationAI #Python #InstructionalDesign #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
Step-by-step build of a working agent with the OpenAI Agents SDK — Agent class, tools, handoffs, tracing — plus an eval pipeline that catches regressions before merge.
Smolagents lets agents write Python instead of JSON. Why code-as-action reduces tool errors and where the security trade-offs are for production deployments.
Modal turns a Python function into autoscaling serverless compute with optional GPU. Deploy a LiveKit Agent with one command and get pay-per-second billing.
Pydantic AI's April release tightens the typed-agent loop and adds structured tool definitions. Why type-safe agents reduce production bugs and speed iteration.
Index a knowledge base with text-embedding-3-large into ChromaDB, expose a retrieve tool to your voice agent, and ground every answer in real documents — full Python tutorial.
Shrink an AI voice agent image from 950MB to 80MB with a Python 3.13 multi-stage build, uv for deps, and gcr.io/distroless/python3 nonroot. Real Dockerfile + benchmarks.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.
Try Live DemoBook a DemoCalculate Your ROI