This feature is currently available to a limited number of early-access customers.
We’ll share further updates once we’re ready to widen availability.
Aptem has a new OData feed for logged activity. If you report on off-the-job training (OTJT) hours, this is the feed to build against. This article covers what it contains, how the fields you use today map across, and the traps to avoid on the way.
- Who this is for: administrators who build or maintain reports from Aptem OData feeds, for example in Power BI or Excel. If nobody at your organisation reports directly from OData, you can skip this article.
On this page
- The feed
- Why move
- What the feed contains
- Mapping from the evidence feed with activity data
- Your history is included
- Reconciling your old and new figures
- Before you build
- Paging and performance
- Feeds being retired
- If the numbers do not look right
Note: the feeds you use today are deprecated and will eventually be withdrawn. They keep working during the transition, and we will give you notice before any of them are withdrawn.
The feed
/odata/1.0/ActivityLog on your own tenant, for example https://yourtenant.aptem.co.uk/odata/1.0/ActivityLog.
It sits on the same OData service, with the same authentication, as the feeds you already use, so nothing about how you connect changes. One row is one logged activity.
A worked example, returning one learner's activity from the start of the year:
/odata/1.0/ActivityLog?$filter=LearnerId eq 12345 and DateOfActivity ge 2026-01-01T00:00:00Z
Access
Reading the feed requires the Allow tutor to read and write Learner's Activity Logs permission, which every basic admin role holds by default. An account that can already read the evidence feeds will normally be able to read this one.
As with the evidence feeds, you only get rows for the learners that account is allowed to see, so two accounts can legitimately return different totals. Deleted activities are excluded.
Why move
Once you opt in, your activity log data will no longer appear in the evidence feed. The activity log feed provides detailed records captured through the activity log. This is the feed you should use.
Which feed you read today determines what you will see.
-
LearningPlanEvidenceandUserEvidences, the standard evidence feeds, will not carry activity log data at any point. Time logged against an evidence item on a programme that is not enabled for activity log continues to appear here exactly as it does now. -
LearningPlanEvidenceWithActivityDataandUserEvidencesWithActivityData, added in February 2026 for early access customers, return evidence and activity log data together. They stay up to date until they are withdrawn.
If you report from the standard evidence feeds, hours logged through the activity log will be missing once a programme is enabled, and your totals will under-count with no error to warn you.
If you report from the feeds with activity data, the hours are there, but they arrive as activity records rather than as ordinary evidence. Any report that filters on evidence-specific fields (evidence kind, evidence name, or the presence of an attachment) will miss them. Those fields are empty or carry no meaning on an activity row.
Both WithActivityData feeds are deprecated and will be withdrawn once customers have moved across, so reporting built on them stops working eventually.
Reading the hours from ActivityLog is the durable fix. It also gives you a cleaner dataset: activity records only, so no evidence rows that carry no recorded time, and no working around the evidence workflow to report on hours.
The feed also carries time from programmes you have not yet enabled. You do not need to build a hybrid model that reads hours from two places depending on a programme's status.
What the feed contains
| Field | Type | What it holds |
|---|---|---|
Id |
int | Unique identifier for the activity. Use this as your key. |
LearnerId |
int | The learner the time belongs to. Joins to the Users feed. |
ComponentId |
int | The learning plan component the time was logged against. |
ActivityType |
enum (text) | The kind of hours: off-the-job training, English and maths, or other. |
Description |
string | The description of the activity. |
Status |
enum (text) | Where the activity is in the review cycle. |
DateOfActivity |
datetime | The date the activity took place. Date only, see below. |
TotalMinutes |
int | Time logged, in minutes. Divide by 60 for hours. |
HoursType |
enum (text) | When the time took place. This is not the same as HoursType on the evidence feeds, see the warning below. |
Source |
enum (text) | Where the activity came from. |
RejectionReason |
string | Why the activity was rejected, where it was. Empty otherwise. |
ExternalSourceId |
string | For activities generated from something else, the identifier of that thing. Empty for time logged directly. |
The coded values
Every coded field returns text, not a number. The value is the name of the option, so Status returns Submitted rather than 0. In the OData metadata these fields appear as enum types, for example Aptem.ActivityLogging.Enums.ActivityType.
The names are fixed and case-sensitive. Match on them exactly, and group any value your report does not recognise rather than dropping the row, so an option added later does not silently remove time from your totals.
ActivityType
| Value | Meaning |
|---|---|
Otjt |
Off-the-job training |
EnglishMaths |
English and maths |
Other |
Anything that is neither of the above |
Status
| Value | Meaning |
|---|---|
Submitted |
Logged and awaiting review |
Accepted |
Approved. Counts towards off-the-job totals |
Rejected |
Not approved. RejectionReason explains why |
Resubmitted |
Amended after rejection and awaiting review again |
Draft |
Set by the platform when time spent is automatically generated by SCORM assets, placements and workshops, and Teams meetings. |
You will not normally see Draft on activity a learner has entered. It appears when activity is logged automatically from SCORM content or a placement and still needs the learner to complete it, and when an activity is reverted because the learning plan component it was logged against has been deleted. In both cases the time does not count until the activity is submitted and accepted.
HoursType
| Value | Meaning |
|---|---|
None |
Not recorded |
DuringWorkingHours |
During working hours |
DuringPaidOwnHours |
Own time, being paid |
DuringToilOwnHours |
Own time, receiving TOIL (time off in lieu) |
DuringUnpaidOwnHours |
Own time, unpaid |
Where no hours type was recorded, HoursType returns None as text, not an empty value. A report that tests for a blank or missing value will not catch these rows.
Source
| Value | Meaning |
|---|---|
ActivityLog |
Logged in the activity log |
Scorm |
From a SCORM asset |
Placement |
From a placement |
Evidence |
Carried over from evidence |
A fifth value, TeamsMeeting, is planned for a future release, for time that comes from a meeting held in Microsoft Teams. Build your report to group unrecognised Source values rather than discard them, so the new value does not drop time from your totals when it arrives.
Mapping from the evidence feed with activity data
This mapping is from LearningPlanEvidenceWithActivityData. That feed is the standard LearningPlanEvidence feed plus five columns: IsActivityLog, ActivityLogId, Description, RejectionReason and Status. If you report from LearningPlanEvidence, those five are not available to you and the rest of the mapping still applies.
Fields that carry over under a new name
| On the evidence feed | On the activity log feed | Note |
|---|---|---|
ActivityLogId |
Id |
Was empty on ordinary evidence rows. Every row now has one. |
SpentTime |
TotalMinutes |
Same meaning, in minutes, new name. |
CompletedDate |
DateOfActivity |
Same meaning. Date only on the new feed. |
SpentTimeType |
HoursType |
Same meaning, new name. Values are the names listed above. |
HoursType |
ActivityType |
Same meaning, new name. Read the warning below. |
Status |
Status |
Still returned as text, and the values match those on ActivityLog. This column carries across without change. |
IsActivityLog |
not needed | Every row in this feed is an activity. |
LearnerId, ComponentId, Description, RejectionReason
|
unchanged | Same names, same meanings. |
Careful with
HoursType. This field name exists in both feeds but has a different meaning in each.On the evidence feed,
HoursTypeis the kind of hours: off-the-job training, English and maths, or other. On the activity log feed that isActivityType.On the activity log feed,
HoursTypeis when the time took place: during working hours, or in the learner's own time. On the evidence feed that isSpentTimeType.A report that carries the name across unchanged will run, return plausible numbers, and be wrong. Map by meaning, not by name.
Fields with no equivalent
| Field | What to do instead |
|---|---|
ProgramId, SubProgramId
|
Join through the learning plan components feed, see below. |
SubmittedById |
Not carried. The feed records the learner the time belongs to, not the account that entered it. |
SubmissionDate |
Not carried. Use DateOfActivity, which is when the activity happened rather than when it was entered. |
UpdatedDate |
Not carried. An incremental refresh keyed on a last-modified date needs rebuilding, most simply as a DateOfActivity window. |
EvidenceName |
Not carried. The nearest equivalent is Description. |
EvidenceKind |
Not needed. Source tells you where an activity came from. |
ConfirmedStatus, LatestStatus
|
Replaced by the single Status field. |
UpdatedDate and SubmittedById are the two most likely to affect an existing Power BI model. Check for them before you start.
Reporting by programme
The activity log feed identifies the learning plan component but not the programme. To report by programme, join ActivityLog.ComponentId to the Id field of the learning plan components feed, which carries ProgramId and SubProgramId.
Join to Id. The field called ComponentId in the learning plan components feed is a different identifier and will not match.
Your history is included
Your existing time records are moved into the activity log, so the feed carries history as well as new activity. Aptem runs this for you, tenant by tenant, and checks the totals before switching any Aptem reporting over. You do not need to do anything to trigger it.
Moved records carry Source Evidence, and ExternalSourceId holds the identifier of the original evidence record, so you can trace any row back.
A small number of historical records are not moved in the first pass: records with no hours, no learning plan component, no hours type, or no record of when the time took place, and records belonging to learners whose data has been anonymised. Nothing is deleted, and these are being handled separately.
Reconciling your old and new figures
Run both side by side for a period you already trust before you retire the old report. You can expect small differences, and check these three causes as potential reasons:
- Held-back records. The historical records described above are not in the feed yet.
- Learner visibility. The feed returns only the learners the reporting account can see, which may differ from the account behind your old report.
-
The
HoursTypeswap. If your totals split by hours type look wrong rather than merely different, this is almost always the cause.
Before you build
-
DateOfActivitycarries no meaningful time. Every row reads midnight UTC. An hours-per-day or shift-level view cannot be built from this field. - Draft activities do not count towards off-the-job totals. Only accepted time counts. Automatically generated activity arrives as a draft for the learner to complete, so it counts once completed and accepted, not when it was generated.
- Hours can arrive without a learner typing them in. Time can reach the activity log from SCORM content or from a placement as well as by direct entry. Compliance reporting that assumes learner-entered hours will mis-count.
-
On programmes not yet enabled, everything is labelled as coming from evidence. Automatically generated time on those programmes carries
SourceEvidence, notScormorPlacement. You can only distinguish learner-entered from generated time on enabled programmes.
Paging and performance
A maximum page size applies, 5,000 rows per request by default. A query with no $top returns the first 5,000 rows with a total count. A $top larger than the limit will be rejected with an explanatory error.
- Build paging in from the start.
- Filter on
DateOfActivityandLearnerIdserver-side. - Import only the tables and columns your report uses.
The OData API performance guide applies unchanged.
Feeds being retired
Two feeds are deprecated. Both were introduced in February 2026 for early access customers and return evidence and activity data together.
| Feed | What is happening |
|---|---|
/odata/1.0/LearningPlanEvidenceWithActivityData |
Deprecated. Move to /odata/1.0/ActivityLog. |
UserEvidencesWithActivityData (via the Users expansion) |
Deprecated. Move to /odata/1.0/ActivityLog. |
Both still work and will keep returning evidence and activity log data up to date. They will be withdrawn once customers have moved across, and we will give you notice before that happens.
Coming in a future release – changes to the standard evidence feeds
LearningPlanEvidence and UserEvidences are not changing in this release, and will not carry activity log data at any point. Some columns within them will be withdrawn in a future release, once customers have moved across:
- On
LearningPlanEvidence:SpentTime,SpentTimeTypeandHoursType. - On
UserEvidences:HoursType.
No date has been set and we will give notice well in advance. If your hours reporting has already moved to /odata/1.0/ActivityLog, no further action will be needed.
If the numbers do not look right
Get in touch with your Aptem contact before changing anything. It helps if you send your tenant, the feed and query you are using, and the two figures that disagree.