Common Recipes

English only

Recipe 1: Sync Users from HRM

A common integration pattern: synchronize users from an external HR system to Pluvo.

For each user in the HRM system:

Step 1: Create or update the user

mutation SyncUser($email: String!, $name: String!, $ref: String!) {
  updateOrCreateUser(
    email: $email
    sendInvite: false
    user: {
      name: $name
      ref: $ref
      roles: [PARTICIPANT]
      language: NL
    }
  ) {
    user { id ref }
    created
  }
}

Step 2: Set custom fields (department, etc.)

mutation SetUserFields($userRef: String!, $department: String!) {
  updateExtraCategoryValues(
    contentType: USER
    modelRef: $userRef
    values: [
      { categoryRef: "department", stringValue: $department }
    ]
  ) {
    extraCategoryValues {
      ... on ExtraCategoryStringValue { stringValue }
    }
  }
}

Step 3: Add to group

mutation AddToGroup($groupRef: String!, $userRef: String!) {
  addUsersToGroup(
    groupRef: $groupRef
    userRefs: [$userRef]
  ) {
    addedIds
    alreadyAddedIds
  }
}

Handling leavers

mutation ArchiveUser($ref: String!) {
  updateOrCreateUser(
    ref: $ref
    user: { archived: true }
  ) {
    user { id archived }
  }
}

Recipe 2: Progress Report for a Group

query GroupProgress($groupId: UUID!, $cursor: String) {
  portfolioReport(
    userGroupIds: [$groupId]
    relatedObjectTypes: [TRAINING]
    first: 50
    after: $cursor
  ) {
    edges {
      node {
        user { id ref name email }
        title
        progress
        score
        completionDate
        timeSpent { days seconds }
      }
    }
    pageInfo { hasNextPage endCursor }
    count
  }
}

Loop through all pages (see Pagination guide) to get the complete report.


Recipe 3: Enroll Users in a Training

Automate enrollment when a user joins a specific group.

Step 1: Listen for GROUP_UPDATED webhook (or run on schedule)

Step 2: Get group members

query GroupMembers($groupRef: String!) {
  group(ref: $groupRef) {
    users(first: 50) {
      edges {
        node { id ref }
      }
    }
  }
}

Step 3: Enroll all members

mutation EnrollGroup($eventRef: String!, $userRefs: [String!]!) {
  createOfferEventEnrollments(
    offerEventRef: $eventRef
    userRefs: $userRefs
    sendMail: false
  ) {
    id
    user { ref }
    status
  }
}
Already-enrolled users are handled gracefully — they won't be duplicated.

Recipe 4: Certificate Monitoring

Track when users earn certificates, e.g. for compliance reporting.

Option A: Webhook-based (real-time)

Listen for the EVENT_CERTIFICATE_ACHIEVED webhook:

{
  "event": "EVENT_CERTIFICATE_ACHIEVED",
  "user": { "id": "...", "name": "...", "ref": "..." },
  "title": "Safety Certificate",
  "training": { "id": "...", "title": "..." },
  "creationDate": "2024-03-15",
  "expiryDate": "2025-03-15"
}

Option B: Query-based (batch)

query RecentCertificates($from: Date!, $to: Date!) {
  portfolioReport(
    relatedObjectTypes: [CERTIFICATE]
    completionDateFrom: $from
    completionDateTo: $to
    first: 50
  ) {
    edges {
      node {
        title
        user { ref name email }
        completionDate
        endDate
      }
    }
    count
  }
}

Recipe 5: Bulk Assign Mandatory Training

Step 1: Get group members

query {
  group(ref: "all-employees") {
    users(first: 50) {
      edges {
        node { ref }
      }
    }
  }
}

Step 2: Add all to training

mutation {
  addUsersToTraining(
    trainingRef: "safety-2024"
    userRefs: ["EMP-001", "EMP-002", "..."]
    sendInvites: true
    role: PARTICIPANT
  ) {
    addedRefs
    alreadyAddedRefs
    failedRefs
  }
}

Recipe 6: Find Inactive Users

query InactiveUsers($before: DateTime!, $cursor: String) {
  paginatedUsers(
    archived: false
    lastLogin_Lt: $before
    first: 50
    after: $cursor
  ) {
    edges {
      node {
        id
        ref
        name
        email
        lastLogin
      }
    }
    pageInfo { hasNextPage endCursor }
    count
  }
}

Variables: { "before": "2024-04-01T00:00:00Z" }


Integration Architecture Tips

  • Use ref everywhere — it's your anchor between systems. Set it on users, groups, trainings, offers, and events.
  • Prefer updateOrCreateUser over separate create/update logic — it handles both cases.
  • Use webhooks for real-time — don't poll the API for changes.
  • Paginate in batches of 50 (the maximum page size) — good balance between speed and reliability.
  • Set sendInvite: false / sendMail: false during bulk operations to avoid notification storms.
  • Cache your OAuth token — it's valid for 24 hours. Request a new one proactively after ~23 hours or when you get a 401.
Schließ die Richtung