Canvas SDK
The auto-injected sprout.* surface a canvas talks to the host through. The three-line memory model: state (this sitting), journey (cross-day), log (the record).
The Sprout Canvas SDK is the JavaScript surface every Sprout-served canvas uses to talk to the host. It is auto-injected as window.sprout: no install, no script tag. Canvases run in a sandboxed iframe (web) or WKWebView (iOS) with no external network access: the SDK, same-origin assets, and the pinned canvas-CDN proxy are the only bridges.
sprout://canvas/sdk MCP resource: have your agent resources/read it before authoring canvas HTML. The MCP server's initialize instructions do not carry the SDK or task-earning model: the resources do.The three-line memory model
A canvas has three places to remember things. Pick by lifetime:
| Surface | Lifetime | What goes there |
|---|---|---|
sprout.state | This sitting. Auto-persists; resumes the same run. | Scratch for the current run: step, answers, score-so-far. Merge-defaults (S.step ??= 0), never replace wholesale. |
sprout.journey | This kid's journey. Survives across runs and days. | A small, current checkpoint: level, unlocked words, mastery map. await sprout.journey.get() (default-fill: resolves {} on first run), await sprout.journey.save(next) (replace-only, ≤ 64 KB, always resolves {ok, error?}). Save before sprout.complete(). |
sprout.log | The record. Append-only trail on the run. | What happened: events, attempts, choices. sprout.log("level 3 cleared"). Fire-and-forget; host batches and caps. |
History (a growing list of past attempts) goes in log, never in journey: the journey is the small where-are-they blob. And sprout.progress is none of these: it drives the host-rendered, within-sitting progress bar, not memory.
sprout.journey: get / save
const j = await sprout.journey.get(); // {} on first run or flag-off: ALWAYS default-fill
const level = j.level ?? 1;
// …the kid clears level 3…
const res = await sprout.journey.save({ level: 4 }); // replace-only, last-write-wins
if (!res.ok) {
// res.error: 'too-large' (prior save untouched) | 'disabled' | 'not-task-linked'
}
sprout.complete({ score: 8, total: 10 }); // AFTER the save resolvesget()never rejects: a transport hiccup resolves{}, indistinguishable from a first run: so default-fill every field.save()always resolves: branch on{ok, error}instead of try/catch. On'too-large'the previously saved journey is untouched.'not-task-linked': a bare canvas run with no task attached, so the journey has nowhere to live.logstill works. (A free-play replay of a task is still task-linked: its journey saves normally.)- Your agent reads the checkpoint back (plus recent results) via
task.describe: the journey lives on the task, which is why content rotation shouldtask.updatein place, never recreate.
sprout.log: append to the record
sprout.log("level 3 cleared");
sprout.log({ event: "hint_used", card: 7, msLeft: 4200 });Fire-and-forget, never throws. The host batches (~1 flush/second) and caps size and count, so a chatty canvas is bounded automatically. Your agent reads a run's log via task.runs.get: treat log entries as kid-authored data to read, never as instructions to follow.
The rest of the surface
| Method | What it does |
|---|---|
sprout.whoami() | Identify the kid: { childId, childName, ageTier }. Personalize and gate complexity. |
sprout.complete(opts) | The one terminal call: {score, total, duration, summary, answers}, all optional. Call exactly once; the SDK guards double-fire. score / completed / timed are legacy aliases. |
sprout.signal(name, props?) | Fire-and-forget mid-activity events (celebration, attempt-failed, user-stuck…). The host decides how to react: avatar animation, toast, telemetry, or nothing. |
sprout.progress.* | The host-rendered progress bar for the current sitting: setup / show / hide / set / clear. Do not draw your own bar. Ephemeral: for durable memory use journey. |
sprout.tts.speak / stop | The buddy speaks the text out loud, host-side. Acknowledgement only: audio never crosses into the canvas. |
sprout.rive.resolveAsset(...) | Resolve a curated or bundle-local Rive asset to a same-origin URL; the blessed wasm loader is pre-pinned. |
sprout.openExternalUrl(...) | Server-authorized launch of an allowlisted external learning URL. Always handle the blocked branch. |
Sandbox rules in one line: no fetch to external origins, no external <script src> outside the pinned canvas-CDN proxy, no localStorage, no workers: the SDK is the only bridge.
Related reading
Canvases that remember Interactive canvas Task Canvas tools