> ## Documentation Index
> Fetch the complete documentation index at: https://docs.timeback.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Single-session activities

> Track activities that students complete in one sitting

Single-session activities are the simplest model: a student starts and completes an activity in one browser session. The client SDK tracks time automatically and reports completion when the student finishes.

<Tip>
  For info on shared concepts, see the [Custom Activities
  overview](/beta/build-on-timeback/sdk/activity-tracking/intro).
</Tip>

## How it works

```mermaid theme={null}
graph LR
    subgraph Client
        A["activity.start()"] --> B["Periodic heartbeats"]
        B --> C["activity.end(metrics)"]
    end
    B -- "TimeSpentEvent" --> D["Caliper API"]
    C -- "Final TimeSpentEvent" --> D
    C -- "ActivityCompletedEvent" --> D
    style A fill:#3b82f6,stroke:#2563eb,color:#fff
    style C fill:#10b981,stroke:#059669,color:#fff
    style D fill:#8b5cf6,stroke:#7c3aed,color:#fff
```

Once started, the SDK sends periodic [TimeSpentEvents](/beta/build-on-timeback/reference/events#timespentevent) throughout the session. When the student finishes, the SDK sends one final [TimeSpentEvent](/beta/build-on-timeback/reference/events#timespentevent) followed by an [ActivityCompletedEvent](/beta/build-on-timeback/reference/events#activitycompletedevent). All events share the same [`runId`](/beta/build-on-timeback/sdk/activity-tracking/intro#event-correlation-with-runid) for correlation.

## Starting an activity

Every single-session activity starts the same way:

```typescript theme={null}
const activity = timeback.activity.start({
	id: 'intro-to-fractions',
	name: 'Introduction to Fractions',
	course: { subject: 'Math', grade: 3 }, // from timeback.config.json
})

// Student engages... (heartbeats emit automatically)
```

<Info>
  See the
  [reference](/beta/build-on-timeback/sdk/activity-tracking/reference#activity-start-params) for
  full parameter documentation including time options, callbacks, and `runId`.
</Info>

## Ending an activity

How you *end* an activity depends on whether the student *completed* the activity. In both cases, accumulated time data is flushed. The difference is whether a completion result is also recorded.

### With completion

When a student finishes, call `activity.end()` with completion metrics:

1. Flushes a final heartbeat ([`TimeSpentEvent`](/beta/build-on-timeback/reference/events#timespentevent))
2. Submits the completion ([`ActivityCompletedEvent`](/beta/build-on-timeback/reference/events#activitycompletedevent))

```typescript theme={null}
await activity.end({
	xpEarned: number,
	totalQuestions: number?,
	correctQuestions: number?,
	masteredUnits: number?,
})
```

<Info>
  See the [reference](/beta/build-on-timeback/sdk/activity-tracking/reference#activity-end-data)
  for full completion data documentation.
</Info>

### Without completion

Call `activity.end()` without arguments to flush time data only; in this case, no completion event is recorded.

1. Flushes a final heartbeat ([`TimeSpentEvent`](/beta/build-on-timeback/reference/events#timespentevent))
2. Stops the heartbeat timer

```typescript theme={null}
await activity.end()
```

There are two reasons to end without completion:

1. **Cleanup**: the component unmounts before the student finishes (e.g. navigating away mid-activity). You want to flush accumulated time without recording a result.
2. **Stateful activities**: the activity spans multiple sessions, so the client only tracks time per visit. Completion is [recorded by the server](/beta/build-on-timeback/sdk/activity-tracking/stateful) when the student eventually finishes.

## Framework integration

<Tabs>
  <Tab title="React">
    ```tsx theme={null}
    import { useTimeback } from '@timeback/sdk/react'
    import { useEffect, useRef } from 'react'

    function LessonPage({ lessonId, lessonName }) {
    	const timeback = useTimeback()
    	const activityRef = useRef(null)

    	useEffect(() => {
    		if (!timeback) return

    		activityRef.current = timeback.activity.start({
    			id: lessonId,
    			name: lessonName,
    			course: { subject: 'Math', grade: 3 },
    		})

    		return () => {
    			// Cleanup flush: time only, do not mark completion on unmount.
    			void activityRef.current?.end()
    		}
    	}, [timeback, lessonId, lessonName])

    	return <div>Lesson content...</div>
    }
    ```
  </Tab>

  <Tab title="Vue">
    ```vue theme={null}
    <script setup>
    import { onMounted, onUnmounted, ref } from 'vue'
    import { useTimeback } from '@timeback/sdk/vue'

    const props = defineProps(['lessonId', 'lessonName'])
    const timeback = useTimeback()
    const activity = ref(null)

    onMounted(() => {
    	if (timeback.value) {
    		activity.value = timeback.value.activity.start({
    			id: props.lessonId,
    			name: props.lessonName,
    			course: { subject: 'Math', grade: 3 },
    		})
    	}
    })

    onUnmounted(() => {
    	// Cleanup flush: do not mark completion on unmount.
    	void activity.value?.end()
    })
    </script>
    ```
  </Tab>

  <Tab title="Svelte">
    ```svelte theme={null}
    <script>
    	import { onMount, onDestroy } from 'svelte'
    	import { timeback } from '@timeback/sdk/svelte'

    	export let lessonId
    	export let lessonName

    	let activity

    	onMount(() => {
    		if ($timeback) {
    			activity = $timeback.activity.start({
    				id: lessonId,
    				name: lessonName,
    				course: { subject: 'Math', grade: 3 },
    			})
    		}
    	})

    	onDestroy(() => {
    		// Cleanup flush: do not mark completion on unmount.
    		void activity?.end()
    	})
    </script>
    ```
  </Tab>

  <Tab title="Solid">
    ```tsx theme={null}
    import { onMount, onCleanup } from 'solid-js'
    import { useTimeback } from '@timeback/sdk/solid'

    function LessonPage(props) {
    	const timeback = useTimeback()
    	let activity

    	onMount(() => {
    		if (!timeback) return

    		activity = timeback.activity.start({
    			id: props.lessonId,
    			name: props.lessonName,
    			course: { subject: 'Math', grade: 3 },
    		})
    	})

    	onCleanup(() => {
    		// Cleanup flush: do not mark completion on unmount.
    		void activity?.end()
    	})

    	return <div>Lesson content...</div>
    }
    ```
  </Tab>
</Tabs>

## Best practices

<AccordionGroup>
  <Accordion title="Start activities in useEffect/onMount">
    Always start activities in lifecycle hooks to ensure proper cleanup.
  </Accordion>

  <Accordion title="Flush time on cleanup, complete on user action">
    Use `activity.end()` (no args) in cleanup functions. Only call `activity.end(metrics)` when
    the student has actually finished the activity.
  </Accordion>

  <Accordion title="Handle missing timeback client">
    Check if the timeback client exists before creating activities (it may be null if not
    [authenticated](/beta/build-on-timeback/sdk/identity)).
  </Accordion>

  <Accordion title="Use meaningful activity IDs">
    Use stable, unique IDs that identify the specific lesson or content piece.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Stateful activities" icon="arrows-rotate" href="/beta/build-on-timeback/sdk/activity-tracking/stateful">
    Multi-session activities with server-side completion
  </Card>

  <Card title="Reference" icon="code" href="/beta/build-on-timeback/sdk/activity-tracking/reference">
    Parameters, properties, methods, and callbacks
  </Card>
</CardGroup>
