---
title: Your page in an activity
description: Put your own web page inside a course activity, record what learners do in it, and let the AI beside it know.
updated: 2026-10-29
---

> All VIABLE docs, for people and their agents: https://developers.viablelab.org/llms.txt

# Your page in an activity

Put your own page, such as a fractions game or a lab simulation, inside a course activity. An agent can sit beside it, and what learners do there goes into the research export.

Courses are set up in the dashboard or with the CLI, not with API keys. Your page never learns who the learner is; VIABLE records its events with the learner.

## Set up the activity

::: steps
1. **Serve your page over https.** Let `https://page.viablelearning.org` frame it, for example with `Content-Security-Policy: frame-ancestors https://page.viablelearning.org`.
2. **Add the panel.** In [Teach → Courses](/dashboard/courses), open the course, add an activity, and add an **Embedded page** panel with your page's address.
3. **Choose what the agent is shown.** If the activity has an agent, pick **Its events**, **Nothing** or **Custom**.
4. **Say how it ends.** Turn on **It says when the learner is done** if your page reports completion. Otherwise learners press "I'm done".
:::

Serve the page from exactly that address: a redirect to another origin cuts the connection. The frame is sandboxed (`allow-scripts allow-same-origin allow-forms allow-popups allow-pointer-lock`) and gets no referrer.

## Add the bridge

Any page works as it is. To record what happens and tell the agent, add a few `postMessage` calls. The panel's **Connect your page (optional)** offers them to copy:

```js
let session = null
window.addEventListener('message', (e) => {
  if (e.origin !== 'https://page.viablelearning.org') return
  if (e.data?.viable === 1 && e.data.type === 'hello') session = e.data.session
})
const viable = (message) => window.parent.postMessage({ viable: 1, ...message }, 'https://page.viablelearning.org')

viable({ type: 'hello' }) // asks for hello again, if you started listening late
viable({ type: 'event', verb: 'answered', object: 'level-2', success: true, score: 1, max_score: 1 })
viable({ type: 'state', data: { level: 2, pieces: 3 } })
viable({ type: 'ask', text: 'Can I get a hint?' })
```

`session` is a random ID for this visit. Ignore other origins.

| `type` | What happens |
| --- | --- |
| `event` | Kept as an event: a `verb` and optionally `object`, `score`, `max_score`, `success`, `completion`, `response`, `duration_ms` and `data`. |
| `state` | What your page shows now, kept at most every 5 seconds. |
| `ask` | Puts `text` in the chat box for the learner to send. Nothing is sent for them. |
| `hello` | Asks for the `hello` again if your page started listening late. |

Prefer xAPI verbs: `attempted`, `answered`, `completed`, `passed`, `failed`, `progressed`, `interacted`. `data` holds up to 20 fields of numbers, true/false values or strings up to 200 characters. With **It says when the learner is done**, the first `completed` event, or one with `completion: true`, counts toward the activity being done.

## What the AI sees

With **Its events**, when a learner asks while your panel is open, the step writing the reply sees your page's last ten events and its latest state. **Custom** sets 0–20 events and whether the state is included. The agent's **Profile** sets where this enters the prompt, and any step can read it as `{{context.workspace}}`. Learners are told on the panel what the agent sees.

## What gets recorded

Events and states are kept with the learner, course, activity and activity version, at every recording level. The research export has them in `workspace_events.csv`. When a study runs in the course, its consent and conditions apply as in any activity.

::: cards
- [Activities](/docs/teach/activities): build the activity around your page.
- [Export](/docs/research/export): download the events with the course's data.
:::
