Skip to content

This document describes the TOML configuration schema for YouLab courses.

v2 (Current) - Recommended format with merged [agent] section and simplified syntax.

v1 (Legacy) - Separate [course] + [agent] sections. See Legacy v1 Schema for migration.

  • Self-contained: Each course.toml is complete without external references
  • Explicit: All configuration is visible in the file
  • AI-friendly: Easy to read and modify programmatically
  • UI-ready: Schema is introspectable for visual editors
config/courses/{course-id}/
├── course.toml # Main course configuration
└── modules/
├── 01-module-name.toml
└── 02-module-name.toml

[agent] - Merged Course & Agent Configuration

Section titled “[agent] - Merged Course & Agent Configuration”

In v2, the [agent] section combines course metadata with agent settings. This eliminates redundancy and makes configs more concise.

FieldTypeRequiredDefaultDescription
idstringyes-Unique course identifier (kebab-case)
namestringyes-Display name
versionstringno”1.0.0”Semantic version
descriptionstringno""Course description
moduleslist[string]no[]Module file names to load (without .toml)
modelstringno”anthropic/claude-sonnet-4-20250514”LLM model identifier
embeddingstringno”openai/text-embedding-3-small”Embedding model
context_windowintno128000Context window size
max_response_tokensintno4096Maximum response tokens
systemstringno""System prompt
toolslist[string]no[]Tool list (see Tools)

Example:

[agent]
id = "college-essay"
name = "College Essay Coaching"
version = "1.0.0"
description = "AI-powered tutoring for college application essays"
modules = ["01-self-discovery", "02-topic-development", "03-drafting"]
model = "anthropic/claude-sonnet-4-20250514"
embedding = "openai/text-embedding-3-small"
context_window = 128000
max_response_tokens = 4096
system = """You are YouLab Essay Coach, an AI tutor specializing in college application essays.
Your approach:
- Guide students through self-discovery exercises
- Help brainstorm and develop essay topics
- Provide constructive feedback on drafts"""
tools = ["send_message", "query_honcho", "edit_memory_block"]

v2 uses a simple string list for tools. Each tool uses its default rule from the registry, or you can override with :rule suffix.

Registry Defaults:

ToolDefault RuleDescription
send_messageexitSend a message to the user
query_honchocontinueQuery conversation history via Honcho dialectic
edit_memory_blockcontinueUpdate a field in the agent’s memory block

Rule Types:

RuleDescription
exitExit agent loop after this tool runs
continueContinue agent loop after this tool
firstRun this tool first in the loop

Syntax:

# Use default rules from registry
tools = ["send_message", "query_honcho", "edit_memory_block"]
# Override specific rules
tools = ["send_message:exit", "custom_tool:continue", "my_init:first"]

Unknown tools default to continue rule.


v2 uses [block.{name}] with field.* dotted keys for a more readable inline syntax.

FieldTypeRequiredDefaultDescription
labelstringyes-Letta’s internal block label (e.g., “persona”, “human”)
descriptionstringno""Block description
sharedboolnofalseEnable cross-agent memory sharing (see Shared Blocks)
field.{name}FieldSchemano-Field definitions (dotted key syntax)

FieldSchema:

FieldTypeDefaultDescription
typeenum-Field type: string, int, float, bool, list, datetime
defaultanytype-specificDefault value
optionslist[string]nullValid values (for dropdowns)
maxintnullMax items for lists
descriptionstringnullField description
requiredboolfalseWhether field is required

Example:

[block.persona]
label = "persona"
shared = false
description = "Agent identity and behavior configuration"
field.name = { type = "string", default = "YouLab Essay Coach", description = "Agent's display name" }
field.role = { type = "string", default = "AI tutor specializing in college application essays" }
field.capabilities = { type = "list", default = ["Guide students", "Provide feedback"], max = 10 }
field.tone = { type = "string", default = "warm", options = ["warm", "professional", "friendly", "formal"] }
field.verbosity = { type = "string", default = "adaptive", options = ["concise", "detailed", "adaptive"] }
[block.human]
label = "human"
shared = false
description = "Student information and session context"
field.name = { type = "string", default = "", description = "Student's name" }
field.role = { type = "string", default = "" }
field.current_task = { type = "string", default = "" }
field.session_state = { type = "string", default = "idle", options = ["idle", "active_task", "waiting_input"] }
field.preferences = { type = "list", default = [], max = 10 }
field.facts = { type = "list", default = [], max = 20 }

When shared = true, the block is created once and reused across all agents using the same course. This enables cross-agent memory sharing for team knowledge, organization context, etc.

How it works:

  1. First agent creation for a course creates the shared block
  2. Subsequent agents attach to the existing block (via block_ids)
  3. Changes by any agent are visible to all agents sharing the block

Use cases:

  • Team knowledge base
  • Organization policies
  • Shared context across tutors

Example:

[block.team]
label = "team"
shared = true
description = "Shared team knowledge across all tutors"
field.policies = { type = "list", default = [], max = 50 }
field.resources = { type = "list", default = [], max = 100 }

Implementation: See AgentManager._get_or_create_shared_block() in src/youlab_server/server/agents.py:53-112


v2 replaces [background.{name}] tables with a [[task]] array. This is more TOML-idiomatic for lists and simplifies the schema.

FieldTypeDefaultDescription
schedulestringnullCron expression (e.g., “0 3 * * *“)
manualbooltrueAllow manual trigger
on_idleboolfalseTrigger on user idle
idle_threshold_minutesint30Minutes of idle before trigger
idle_cooldown_minutesint60Cooldown between idle triggers
agent_typeslist[string][“tutor”]Agent types to process
user_filterstring”all”User filter expression
batch_sizeint50Users per batch
querieslist[QueryConfig][]Dialectic queries to run
systemstringnullCustom system prompt for task agent
toolslist[string][]Additional tools for task agent

QueryConfig:

FieldTypeDefaultDescription
targetstring-Target in “block.field” format
questionstring-Question to ask about conversation
scopeenum”all”Session scope: “all”, “recent”, “current”, “specific”
recent_limitint5Number of recent sessions (if scope is “recent”)
mergeenum”append”Merge strategy: “append”, “replace”, “llm_diff”

Example:

[[task]]
schedule = "0 3 * * *" # 3 AM daily
manual = true
agent_types = ["tutor", "college-essay"]
user_filter = "all"
batch_size = 50
queries = [
{ target = "human.context_notes", question = "What learning style works best?", scope = "all", merge = "append" },
{ target = "human.facts", question = "How engaged is this student?", scope = "recent", recent_limit = 5, merge = "append" },
{ target = "persona.constraints", question = "How should I adjust my style?", scope = "all", merge = "llm_diff" }
]
[[task]]
schedule = "0 12 * * 0" # Sundays at noon
manual = false
system = "You are an analytics agent that summarizes weekly progress."
tools = ["query_honcho"]
queries = [
{ target = "human.facts", question = "Summarize this week's progress", scope = "recent", recent_limit = 7, merge = "replace" }
]

FieldTypeDefaultDescription
welcome_firststring”Hello! How can I help you today?”First-time user greeting
welcome_returningstring”Welcome back!”Returning user greeting
error_unavailablestring”I’m temporarily unavailable…”Error message

Example:

[messages]
welcome_first = "Welcome to YouLab! I'm your Essay Coach."
welcome_returning = "Welcome back! Ready to continue?"
error_unavailable = "I'm having a moment - please try again in a few seconds."

Module files define the curriculum structure with steps.

FieldTypeRequiredDefaultDescription
idstringyes-Module identifier
namestringyes-Display name
orderintno0Sort order
descriptionstringno""Module description
FieldTypeRequiredDefaultDescription
idstringyes-Step identifier
namestringyes-Display name
orderintno0Sort order within module
descriptionstringno""Step description
objectiveslist[string]no[]Learning objectives
FieldTypeDefaultDescription
required_fieldslist[string][]Fields that must be non-empty (e.g., “human.name”)
min_turnsintnullMinimum conversation turns
min_list_lengthdict[string, int]{}Minimum items in list fields
auto_advanceboolfalseAuto-advance when complete

[steps.agent] - Step-Specific Agent Config

Section titled “[steps.agent] - Step-Specific Agent Config”
FieldTypeDefaultDescription
openingstringnullOpening message for this step
focuslist[string][]Topics to focus on
guidancelist[string][]Guidance for the agent
persona_overridesdict{}Override persona fields for this step

Example module file:

[module]
id = "01-self-discovery"
name = "Self-Discovery"
order = 1
description = "Explore who you are and what matters to you"
[[steps]]
id = "welcome"
name = "Welcome & Onboarding"
order = 1
objectives = ["Learn student's name", "Set expectations"]
[steps.completion]
required_fields = ["human.name"]
min_turns = 3
[steps.agent]
opening = "Welcome! What's your name?"
focus = ["introduction", "goals"]

The curriculum system exposes HTTP endpoints for management:

  • GET /curriculum/courses - List all courses
  • GET /curriculum/courses/{id} - Get course summary
  • GET /curriculum/courses/{id}/full - Get complete config as JSON
  • GET /curriculum/courses/{id}/modules - Get modules with steps
  • POST /curriculum/reload - Hot-reload all configurations

# =============================================================================
# AGENT CONFIGURATION (v2 schema - combines course + agent)
# =============================================================================
[agent]
id = "college-essay"
name = "College Essay Coaching"
version = "1.0.0"
description = "AI-powered tutoring for college application essays"
modules = ["01-self-discovery", "02-topic-development", "03-drafting"]
model = "anthropic/claude-sonnet-4-20250514"
system = """You are YouLab Essay Coach, an AI tutor."""
tools = ["send_message", "query_honcho", "edit_memory_block"]
# =============================================================================
# MEMORY BLOCKS (v2 format - field.* dotted keys)
# =============================================================================
[block.persona]
label = "persona"
description = "Agent identity and behavior"
field.name = { type = "string", default = "Essay Coach" }
field.tone = { type = "string", default = "warm", options = ["warm", "professional"] }
field.capabilities = { type = "list", default = ["Guide students"], max = 10 }
[block.human]
label = "human"
description = "Student information"
field.name = { type = "string", default = "" }
field.facts = { type = "list", default = [], max = 20 }
# =============================================================================
# BACKGROUND TASKS (v2 format - [[task]] array)
# =============================================================================
[[task]]
schedule = "0 3 * * *"
queries = [
{ target = "human.facts", question = "What motivates this student?", merge = "append" }
]
# =============================================================================
# UI MESSAGES
# =============================================================================
[messages]
welcome_first = "Welcome to YouLab!"
welcome_returning = "Welcome back!"

Deprecated: v1 schema is supported for backwards compatibility but should be migrated to v2.

Featurev1v2
Course metadata[course] sectionMerged into [agent]
Block syntax[blocks.x.fields.y] nested tables[block.x] with field.y = {...}
Tool config[[agent.tools]] with explicit rulestools = ["name"] with registry defaults
Background agents[background.name] tables[[task]] array
Shared blocksNot supportedshared = true flag
# v1: Separate [course] and [agent] sections
[course]
id = "college-essay"
name = "College Essay Coaching"
modules = ["01-self-discovery"]
[agent]
model = "anthropic/claude-sonnet-4-20250514"
system = "You are an essay coach."
# v1: Explicit tool configuration
[[agent.tools]]
id = "send_message"
rules = { type = "exit_loop" }
[[agent.tools]]
id = "query_honcho"
rules = { type = "continue_loop" }
# v1: Nested block fields
[blocks.persona]
label = "persona"
[blocks.persona.fields]
name = { type = "string", default = "Essay Coach" }
tone = { type = "string", default = "warm" }
# v1: Named background agent sections
[background.insight-harvester]
enabled = true
agent_types = ["tutor"]
[background.insight-harvester.triggers]
schedule = "0 3 * * *"
[[background.insight-harvester.queries]]
id = "learning_style"
question = "What learning style works best?"
target_block = "human"
target_field = "context_notes"
merge_strategy = "append"
  1. Merge [course] into [agent]: Move id, name, version, description, modules from [course] to [agent]

  2. Simplify tools: Replace [[agent.tools]] with tools = ["name", ...]

  3. Update block syntax: Change [blocks.x.fields.y] to [block.x] with field.y = {...}

  4. Convert background agents: Replace [background.name] with [[task]] array

  5. Add shared flag: For cross-agent blocks, add shared = true

The loader (CurriculumLoader) automatically detects and handles both formats.