Migrating from the Data API

The legacy Data API provided SQL-based access to learner data via POST /api/emc/reports/query with a SQL query body. The new GraphQL API replaces this with structured, filterable endpoints.

Column mapping — roster_v1

The most commonly used Data API table is roster_v1. Here is how its columns map to GraphQL fields:

roster_v1 columnGraphQL fieldAPINotes
emailuserEmailProgram Progress
first_nameuserFirstNameProgram Progress
last_nameuserLastNameProgram Progress
user_keyuserIdProgram Progress
nd_titleCatalogQuery separately: catalog { programs { key title } }
node_keyprogramKeyProgram Progress
enrollment_stateenrollmentStatusProgram ProgressEnum: ENROLLED, UNENROLLED, GRADUATED, STATIC_ACCESS
enrolled_atenrolledAtProgram ProgressRFC 3339 timestamp
graduated_atgraduatedAt / completedAtProgram ProgressNullable — only set when graduated/completed
last_seenlastActiveAtProgram ProgressNullable
on_track_statusonTrackStatusProgram ProgressEnum: ON_TRACK, OFF_TRACK, MONITOR, COMPLETED, READY_TO_GRADUATE, GRADUATED
program_typeprogramTypeProgram ProgressEnum: NANODEGREE, COURSE, PART
core_projects_passedprojectsRequiredCompletedProgram ProgressApproximate mapping
total_core_projectsprojectsRequiredTotalProgram ProgressApproximate mapping

SQL → GraphQL examples

Graduated learners since a date

Data API (SQL):

SELECT email, nd_title, graduated_at::DATE
FROM roster_v1
WHERE graduated_at IS NOT NULL
AND graduated_at::date > '2026-01-01'::DATE

GraphQL:

query GraduatedLearners($input: ProgramProgressInput!) {
  programProgress(input: $input) {
    totalCount
    pageInfo { hasNextPage endCursor }
    edges {
      node {
        userEmail
        programKey
        enrollmentStatus
        graduatedAt
      }
    }
  }
}

Variables:

{
  "input": {
    "filter": { "enrollmentStatus": ["GRADUATED"] },
    "orderBy": "LAST_ACTIVE",
    "order": "DESC",
    "first": 100
  }
}

Note: The GraphQL API filters by enrollment status but does not support arbitrary date range filtering on graduatedAt. Filter to graduated learners server-side, then check graduatedAt values client-side if you need a specific date cutoff.

Currently enrolled learners, sorted by activity

Data API (SQL):

SELECT email, last_seen, enrollment_state
FROM roster_v1
WHERE enrollment_state = 'enrolled'
ORDER BY last_seen ASC

GraphQL:

query EnrolledByActivity($input: ProgramProgressInput!) {
  programProgress(input: $input) {
    edges {
      node {
        userEmail
        lastActiveAt
        enrollmentStatus
      }
    }
    pageInfo { hasNextPage endCursor }
  }
}

Variables:

{
  "input": {
    "filter": { "enrollmentStatus": ["ENROLLED"] },
    "orderBy": "LAST_ACTIVE",
    "order": "ASC",
    "first": 100
  }
}

Coverage gaps

Some roster_v1 and projects_v1 columns are not yet available in the GraphQL API:

Data API columnStatus
projects_v1 (individual project submissions)Not available — only aggregate counts via projectsRequiredTotal / projectsRequiredCompleted
total_concepts / total_concepts_completedNot available — use completionPercentage instead
employee_id (external ID)Not available
tag_ids / tag_names (EMC groups)Not available
enrollment_history (state changes over time)Not available — only current state is returned

If your Data API integration depends on fields not yet available in GraphQL, contact your Udacity account team for guidance on timing and alternatives.

Key differences from the Data API

  • No SQL — you write structured GraphQL queries instead of raw SQL
  • Cursor-based pagination instead of LIMIT / OFFSET — more reliable for large or changing datasets (see Pagination)
  • Separate endpoints per domain — catalog, program progress, assessment progress, and learning plan progress are each queried independently
  • Token auth — uses Authorization: Token <api_key> with a COMPANY scope
  • Selective fields — request only the fields you need, reducing payload size
  • Enum values — statuses use UPPER_CASE enums (e.g., ENROLLED not enrolled)

Getting started