Guides / Use cases
Tutoring and education
For tutoring apps, homework helpers and learning platforms: a tutor that knows what each learner has mastered, what they keep mixing up and what they are working towards, across every lesson. Recall stores the lessons, learns about the learner in the background, and gives your tutor and the teacher a learner model your app can use.
What memory adds
Without memory, every lesson starts from the syllabus. With it, the tutor picks up where the learner is:
| Without memory | With Recall | |
|---|---|---|
| Week 1, fractions lesson | The learner says 1/8 is bigger than 1/4, "because 8 is bigger than 4". | |
| Week 2, homework | "Let's start with fractions. A fraction has a numerator and a denominator..." | "Before we start: last week bigger denominators tripped you up. Which is bigger, 1/6 or 1/3?" |
| Week 4, the teacher looks | Twelve chat logs to read. | Mastered: comparing unit fractions. Still mixing up: adding fractions with different denominators. Next: common denominators. Search finds the lesson behind each. |
Recall does not teach. Your own model writes the explanations, with your teaching prompt and your safeguards; Recall gives it what this learner knows and gets wrong, and learns from each lesson.
How to model it
| In your product | In Recall | Why |
|---|---|---|
| A school, per school year | Workspace, e.g. school-2207-y2026 | Each school's data stays apart, and the end of the year is one delete. |
| A learner | Peer learner-5531 | A pseudonymous id from your own system. Recall learns about the learner from what they write. |
| Your tutor | Peer tutor, with observeMe: false | Recall does not learn from the tutor's own lines, only reads them as context, so they cost less. |
| A lesson or homework session | Session, e.g. lesson-40817 | Context and summaries per lesson; a teacher can see which lesson something came from. |
| A course | Scope course-math-7, given when each session is created | The course's own view of the learner, so maths progress is not mixed with French. |
| What the learner knows and gets wrong | Conclusions and the peer card | Learned in the background. A fact that stops being true is closed, not lost. |
A consumer app with no school in between uses one workspace for the app and one peer per learner. With a single subject you can skip the scope and ask the learner peer directly.
Build it
Install the SDK (npm install @recall-memory/sdk or pip install recall-mem) and set RECALL_API_KEY. Call Recall from your server, not from the learner's device. Create the learner, the tutor and a session per lesson, filed under its course.
import { Recall } from "@recall-memory/sdk";
const recall = new Recall({ workspaceId: "school-2207-y2026" });
// A pseudonymous id from your own system: no name, email or class in Recall.
const learner = recall.peer("learner-5531");
// The tutor is your app, so Recall learns about the learner, not from the tutor's own lines.
const tutor = recall.peer("tutor", { configuration: { observeMe: false } });
// One session per lesson or homework session, filed under the course it belongs to.
const course = "course-math-7";
const lesson = recall.session("lesson-40817", { peers: [learner, tutor], scopes: [course] });
from pydantic import BaseModel, Field
from recall_memory import PeerConfig, Recall
recall = Recall(workspace_id="school-2207-y2026")
# A pseudonymous id from your own system: no name, email or class in Recall.
learner = recall.peer("learner-5531")
# The tutor is your app, so Recall learns about the learner, not from the tutor's own lines.
tutor = recall.peer("tutor", configuration=PeerConfig(observe_me=False))
# One session per lesson or homework session, filed under the course it belongs to.
course = "course-math-7"
lesson = recall.session("lesson-40817", peers=[learner, tutor], scopes=[course])
Every learner message: one request. turn stores the message and returns the context for the tutor's reply in the same round trip: the lesson so far, a summary of older turns, and what Recall has learned about the learner in earlier lessons. It uses no model, so it is fast and included in the price.
// Each learner message: store it and get back what the tutor should know, in one request.
const { context } = await lesson.turn(
[learner.message("1/8 is bigger than 1/4, right? Because 8 is bigger than 4.")],
{ peerTarget: learner, tokens: 2000 },
);
const prompt = context.toOpenAI(tutor); // what Recall knows about the learner, then the lesson
// Your model writes the tutor's reply from `prompt`; store it so the lesson stays complete.
await lesson.addMessages([
tutor.message("One pizza cut into 8 slices, one cut into 4. Which slice is bigger?"),
]);
# Each learner message: store it and get back what the tutor should know, in one request.
result = lesson.turn(
[learner.message("1/8 is bigger than 1/4, right? Because 8 is bigger than 4.")],
peer_target=learner,
tokens=2000,
)
prompt = result.context.to_openai(assistant=tutor) # the learner's memory, then the lesson
# Your model writes the tutor's reply from `prompt`; store it so the lesson stays complete.
lesson.add_messages(
[tutor.message("One pizza cut into 8 slices, one cut into 4. Which slice is bigger?")]
)
The learner model, as JSON. Ask in plain language and pass responseFormat: the answer comes back as JSON that matches your schema, ready for a progress screen or for choosing the next exercise. In TypeScript pass a JSON Schema (you get the parsed object) or { jsonSchema, parse } for a typed result; in Python pass a Pydantic class (you get an instance) or a JSON-schema dict (you get the JSON text). scope answers from the course's view of the learner. This is one model call of a few seconds, so run it after a lesson and store the result rather than asking on every page view.
// Before the next lesson: the learner model as JSON your app can show and act on.
// (waitForIdle is for this demo; by the next lesson, learning has long finished.)
await recall.waitForIdle();
type LearnerModel = { mastered: string[]; misconceptions: string[]; nextGoal: string };
const list = { type: "array", items: { type: "string" } };
const learnerModel = {
jsonSchema: {
type: "object",
properties: {
mastered: { ...list, description: "Skills the learner has shown they can do" },
misconceptions: { ...list, description: "Mistakes the learner still makes" },
nextGoal: { type: "string", description: "The one thing to practise next" },
},
required: ["mastered", "misconceptions", "nextGoal"],
},
// With zod: { jsonSchema: z.toJSONSchema(Schema), parse: (data) => Schema.parse(data) }
parse: (data: unknown) => data as LearnerModel,
};
const model = await learner.chat(
"What has this learner mastered, what do they still get wrong, and what should come next?",
{ scope: course, reasoningLevel: "low", responseFormat: learnerModel },
);
# Before the next lesson: the learner model as JSON your app can show and act on.
# (wait_for_idle is for this demo; by the next lesson, learning has long finished.)
recall.wait_for_idle()
class LearnerModel(BaseModel):
mastered: list[str] = Field(description="Skills the learner has shown they can do")
misconceptions: list[str] = Field(description="Mistakes the learner still makes")
next_goal: str = Field(description="The one thing to practise next")
model = learner.chat(
"What has this learner mastered, what do they still get wrong, and what should come next?",
scope=course,
reasoning_level="low",
response_format=LearnerModel,
)
Misconceptions that get corrected. Facts in Recall have a start and an end. When a later lesson shows the learner now gets it right, Recall closes the old fact ("thinks 1/8 is bigger than 1/4") and starts a new one, the same way it handles someone moving house. Prompt context and the learner model use what holds now. The old fact stays as history, so chat can answer "what has this learner corrected?", and the dashboard's memory explorer shows when each fact held.
// Two weeks later, in homework, the learner has it right. Recall closes the old fact instead
// of deleting it: context and the learner model show what holds now; chat can say what changed.
const homework = recall.session("homework-40822", { peers: [learner, tutor], scopes: [course] });
await homework.addMessages([
learner.message("Wait, 1/8 is smaller than 1/4. More slices means smaller slices."),
]);
await recall.waitForIdle(); // demo only, as above
const progress = await learner.chat(
"Which misconceptions has this learner corrected, and when?",
{ scope: course, reasoningLevel: "low" },
);
# Two weeks later, in homework, the learner has it right. Recall closes the old fact instead
# of deleting it: context and the learner model show what holds now; chat can say what changed.
homework = recall.session("homework-40822", peers=[learner, tutor], scopes=[course])
homework.add_messages(
[learner.message("Wait, 1/8 is smaller than 1/4. More slices means smaller slices.")]
)
recall.wait_for_idle() # demo only, as above
progress = learner.chat(
"Which misconceptions has this learner corrected, and when?",
scope=course,
reasoning_level="low",
)
The teacher's view. Search finds what the learner wrote about a topic across all their sessions, with the session each message came from, so a teacher can check where a line in the learner model came from. For a written overview of one learner, ask chat as above. Give teachers this view inside your app; they do not need Recall keys.
// The teacher's view: the learner's own words about a topic, across every lesson, with the
// session each came from. Search takes about a second and is included in the price.
const found = await learner.search("fractions pizza slices", { limit: 20 });
const excerpts = found.map((m) => ({ lesson: m.sessionId, at: m.createdAt, said: m.content }));
# The teacher's view: the learner's own words about a topic, across every lesson, with the
# session each came from. Search takes about a second and is included in the price.
found = learner.search("fractions pizza slices", limit=20)
excerpts = [{"lesson": m.session_id, "at": m.created_at, "said": m.content} for m in found]
A learner leaves, or the school year ends. learner.delete() erases the learner's messages and everything learned about them. The tutor's replies in their lessons stay unless you delete those sessions too, and they often quote the learner, so delete both. With a workspace per school year, the end of the year is one loop.
// A learner leaves: delete their sessions (the tutor's replies quote them), then the learner.
for (const s of await (await learner.sessions()).toArray()) await s.delete();
await learner.delete();
// End of the school year: every session, then the workspace (refused while sessions remain).
for (const s of await (await recall.sessions()).toArray()) await s.delete();
await recall.deleteWorkspace();
# A learner leaves: delete their sessions (the tutor's replies quote them), then the learner.
for s in list(learner.sessions()):
s.delete()
learner.delete()
# End of the school year: every session, then the workspace (refused while sessions remain).
for s in list(recall.sessions()):
s.delete()
recall.delete_workspace(recall.workspace_id)
Settings that matter
| Setting | Use |
|---|---|
observeMe: false on the tutor | Recall learns about the learner only. The tutor's lines are read as context, not learned from, so they cost less. |
scopes on each session, scope on chat | A view of the learner built only from one course's sessions. Both need a workspace key, not a key scoped to one learner. |
responseFormat | JSON for the learner model. The root must be an object; allOf, not and conditional keywords are not supported. Field descriptions help the model fill each field. |
reasoningLevel | low (the default) for the learner model, minimal for quick checks such as "has this learner met common denominators?". See Reasoning levels and models. |
Workspace reasoning.customInstructions | Keep learning on the subject, for example: "Record what the learner knows, gets wrong and is working on. Do not record family, health or other personal matters." It steers the model that learns; it is not a filter. |
tokens | The context budget for the tutor's prompt. Older turns become a summary, so a long lesson stays within it. |
Message createdAt | When you import past homework, pass when it was written, so "corrected in week 3" is dated right. |
Cost and speed
Measured from Europe against the hosted API (Frankfurt), October 2026: a turn or context call took 0.3 to 0.7 seconds, a search about 0.9 seconds, and a question at minimal or low 2 to 4.5 seconds. Learning happens in the background and does not hold up the lesson.
Context, search, storage and summaries are included. You pay for learning, $1.75 per million message tokens with every message counting as at least 50, and for questions. A learner with three lessons a week (12 a month), each with 20 short messages of their own and 20 tutor replies of about 80 tokens, a learner model after every lesson and four teacher questions a month:
| Item | Per month |
|---|---|
| Learner messages: 12 × 20 × 50 tokens × $1.75 / 1M | $0.0210 |
| Tutor replies, read but not learned from: 12 × 20 × 80 tokens × $1.75 / 1M | $0.0336 |
Learner model at low: 12 × $0.005 | $0.0600 |
Teacher questions at low: 4 × $0.005 | $0.0200 |
| Total | about $0.13 per learner a month |
Answers are cached until the memory they depend on changes, but a cached answer is still billed as one query, so store the learner model instead of asking again. More in Cost and speed.
Privacy and forgetting
Learners are often children. This is practical guidance, not legal advice: check it against the law that applies to you and to the schools you serve, and see Privacy, consent and forgetting.
- Know who decides. When a school uses your tutor, the school is usually the controller of its learners' data and you process it on the school's behalf, with Recall as your sub-processor. Sign a data processing agreement with each school that names Recall; our DPA covers this case. Expect the school to ask about a data protection impact assessment.
- Get consent where it is required. If you offer the tutor directly to children and rely on consent, get a parent's consent where the law requires it, before the first lesson is recorded. The age at which a child can consent alone differs by country.
- Store less. Use pseudonymous ids, never names or email addresses, and keep class lists and contact details in your own system. Children overshare: keep learning on the subject with a custom instruction, and do not send messages your tutor does not need, such as a chat about home.
- Tell learners and parents. In plain words: the tutor remembers what the learner worked on and got wrong, to pick up where they left off. The text is stored in the EU (Frankfurt) and processed by AI model providers in the US through OpenRouter, restricted to providers that do not collect the data, and it is not used to train models. Show them the learner model.
- Delete on a schedule. Delete a learner who leaves, and delete the year's workspace when the school's retention period ends. If the school wants a copy first, export it under Settings.
Recall's own terms are in the privacy policy and the DPA.
Pitfalls
- Names as ids. Ids appear in URLs, logs and exports and cannot be renamed. Use your own pseudonymous id.
- Asking on every page view. The learner model takes seconds and every request is billed, cached or not. Compute it after a lesson and store it.
- All subjects in one memory. Without course scopes, "what has this learner mastered?" mixes maths and French. Give each session its course scope when you create it.
- Deleting the learner but not the lessons. The tutor's replies stay and often quote the learner. Delete their sessions too.
- Treating the learner model as a grade. A model writes it from the conversation, and it can be wrong. Let a teacher decide anything that affects grades or placement.
- Expecting Recall to teach. Recall is memory, not the tutor: bring your own model, teaching prompt and safeguards for children.
Next: Health, fitness and coaching · Privacy, consent and forgetting