Skip to main content
Stateful activities span multiple sessions. The client SDK tracks time per session via heartbeats, and the server records completion when the activity is truly done.
For info on shared concepts, see the Activity Tracking overview.

Architecture

Stateful activities split responsibility between frontend and backend: A runId ties all sessions together. The SDK generates one automatically when you call activity.start(), but you can also provide your own. The SDK HTTP handlers and server recording API require a UUID. Reuse an assignment ID only if it is a valid UUID; otherwise store a separate UUID for the attempt. 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: your app saves progress independently of the SDK. Calling activity.end() attempts a final time flush and closes this browser tracker without a completion event. To resume, load saved progress from your app and create a new tracker with the same runId. Tab closure does not guarantee a successful final save or heartbeat.
The frontend always calls activity.end() without metrics. Completion is recorded by the server.

Loading and lifecycle

Create or load the attempt in your authenticated backend before starting its browser tracker. Persist the UUID with the attempt, and return it with the saved answers. This avoids losing the correlation ID if a browser closes between start() and a later save. Use the lifecycle guidance in Single-session activities: wait for client initialization and verification, serialize end() and start(), and cancel pending progress loads when a screen unmounts. The SDK does not provide saveProgress or loadProgress; both are your application APIs.

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 passing time: false. The client still provides the activity.end() ergonomics, but no TimeSpentEvents are sent.
When using time: false, the server must report time via the time parameter in timeback.activity.record(). Do not use both heartbeats and server-side time — this will double-count. See the server reference for details.

Backend

When a student completes an activity, as determined by your app’s logic, the backend records the result using timeback.activity.record(). This sends an ActivityCompletedEvent to the Caliper API after resolving the user and synced course. Downstream gradebook and XP processing is asynchronous; a successful SDK call does not prove that it has completed. At 100% progress the SDK also attempts a mastery-completion gradebook entry; failures of that extra write are logged without failing the request. Pass the same runId from the frontend to correlate heartbeats with the completion event.
See the reference for full parameter documentation.
Only include time when the frontend uses time: false. If heartbeats are active (the default), the platform already has the time data — passing time here would double-count. See the full server reference for all parameters.

Best practices

The runId is the correlation key between frontend heartbeats and backend completion. Save it to your database as soon as the activity starts.
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.
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