Architecture
Stateful activities split responsibility between frontend and backend:| Responsibility | Owner | Mechanism |
|---|---|---|
| Time tracking | Frontend or Backend | Heartbeats or time: false |
| State | Backend | Your app’s database |
| Completion | Backend | timeback.activity.record() |
runId ties all sessions together. The SDK generates one automatically when you call activity.start(), but you can also provide your own. Any unique string works. If your app already has a unique identifier for the activity attempt (e.g. an assignment row ID), pass it as runId directly instead of storing a separate value. On resume, pass the same runId so every heartbeat and the final completion event share the same identifier.
Frontend
The frontend tracks time per session and orchestrates the student’s workflow (starting, resuming, and submitting answers). But completion must come from the server: across multiple sessions, it’s the only part of the system that has the full picture of the student’s accumulated progress.Starting a session
See the reference for full parameter documentation including time options and callbacks.
Resuming a session
Ending a session
This is the key difference from single-session activities: the student can leave at any point — close the tab, navigate away, or simply stop for the day — without losing their progress. Callingactivity.end() flushes the accumulated time but does not record a completion. The activity stays open, ready to be resumed later with the same runId.
The frontend always calls
activity.end() without metrics. Completion is recorded by the
server.Framework examples
- React
- Vue
- Svelte
- Solid
Server-managed time
If your server already tracks time (e.g. from request logs or its own session model), you can disable heartbeats entirely by passingtime: false. The client still provides the activity.end() ergonomics, but no TimeSpentEvents are sent.
Backend
When a student completes an activity, as determined by your app’s logic, the backend records the result usingtimeback.activity.record(). This sends an ActivityCompletedEvent to the Caliper API and runs the full pipeline including gradebook and XP.
Pass the same runId from the frontend to correlate heartbeats with the completion event.
See the reference for full parameter documentation.
Best practices
Persist runId immediately after activity.start()
Persist runId immediately after activity.start()
The
runId is the correlation key between frontend heartbeats and backend completion. Save
it to your database as soon as the activity starts.Use a fresh runId for new attempts after completion
Use a fresh runId for new attempts after completion
When a student starts a new attempt of a previously completed activity, do not reuse the old
runId. Omit it from activity.start() to generate a fresh one — otherwise new heartbeats
would be incorrectly correlated with the old completion event.Only call activity.end() without args on the frontend
Only call activity.end() without args on the frontend
Completion should come from the backend via
timeback.activity.record(). The frontend
should always use activity.end() (no metrics) to flush time data only.Next steps
Single-session activities
Simpler model for one-sitting activities
Reference
Full parameter and method reference
Identity
Authentication setup for resolving users