Consulting Mutations
Mutation operations for managing consulting services and their time-slot meetings in your Loopwise school
The consulting mutations let you programmatically manage consulting services (1-on-1 / group coaching offerings) and the time-slot meetings booked under them.
Endpoint and authentication. All admin mutations live on
POST /admin/graphql— not the/graphqlendpoint, which is the public school API. Authenticate with ansk_live_…key in theX-Teachify-API-Keyheader. Service mutations require thecourses:writescope; meeting-enrollment mutations requirestudents:write(ormembers:write).
Service entity mutations (require courses:write):
createConsultingService— Create a new consulting service under a courseupdateConsultingService— Partial update of an existing servicedeleteConsultingService— Soft-delete a service
Meeting (time-slot) mutations (require courses:write):
bulkCreateConsultingMeetings— Create multiple time slots under a service in one round tripupdateConsultingMeeting— Partial update of a single meetingcancelConsultingMeeting— Cancel a single meetingbulkCancelConsultingMeetings— Cancel multiple meetings in one round trip
Meeting enrollment mutations (require students:write or members:write):
enrollStudentToConsultingMeeting— Add a student to a meetingremoveStudentFromConsultingMeeting— Remove a student from a meeting
All mutations return an errors field with any validation or processing failures. Field names use camelCase on the wire; type names in this document use the GraphQL schema name (e.g. AdminConsultingServiceInput).
Create a Consulting Service
The createConsultingService mutation creates a new consulting service under an existing course.
Input Parameters
| Field | Type | Description |
|---|---|---|
input | AdminConsultingServiceInput! | Input object |
AdminConsultingServiceInput Fields
| Field | Type | Required | Description |
|---|---|---|---|
name | String! | Yes | Display name of the service |
courseId | String! | Yes | Parent course ID. Course must belong to the same school |
slug | String | No | URL slug (lowercase letters, numbers, hyphens). If omitted, derived from name by friendly_id |
description | String | No | Service description |
lecturerId | String | No | Assigned lecturer ID. Must belong to the same school |
published | Boolean | No | When true, marks the service as published and stamps publishedAt to the current time |
tags | [String!] | No | Tag list stored in settings JSON. Replaces the entire list — pass all tags every time |
ratingFormId | String | No | Linked feedback form ID |
backgroundColor | String | No | UI background color (hex) |
Return Type
type AdminConsultingServiceCreatePayload {
consultingService: AdminConsultingService
errors: [String!]
}Example
mutation CreateConsultingService {
createConsultingService(input: {
name: "1-on-1 Career Coaching"
courseId: "ab6ee332-614d-475d-93b5-abc5ebb1fc84"
slug: "career-coaching"
description: "30-minute career coaching session"
lecturerId: "f1b2c3d4-5678-90ef-abcd-ef1234567890"
published: true
tags: ["career", "coaching"]
}) {
consultingService {
id
name
slug
published
publishedAt
effectiveTimezone
}
errors
}
}Sample Response
{
"data": {
"createConsultingService": {
"consultingService": {
"id": "35a72dba-c3cc-4a06-b7eb-d784d010386e",
"name": "1-on-1 Career Coaching",
"slug": "career-coaching",
"published": true,
"publishedAt": 1747397869,
"effectiveTimezone": "Asia/Taipei"
},
"errors": null
}
}
}Common Errors
| Error code | Message |
|---|---|
CONSULTING-002 | Parent course not found or not in this school |
CONSULTING-003 | Slug must only contain lowercase letters, numbers, and hyphens |
CONSULTING-005 | Lecturer not found or not in this school |
CONSULTING-006 | Rating form not found or not in this school |
Update a Consulting Service
The updateConsultingService mutation applies a partial update. Only provided keys are changed; explicit null clears nullable fields (lecturerId, ratingFormId, backgroundColor).
Not updatable:
courseIdis intentionally read-only. Reparenting a service across courses would orphan its meetings and enrollments — create a new service instead.Publish-state semantics: setting
published: falsedoes not clearpublishedAt; the historical first-publish timestamp is preserved.Lecturer reassignment: changes the service-level lecturer only. Existing meetings keep the lecturer they were created with — update each meeting individually if you need to reassign past or scheduled slots.
Input Parameters
| Field | Type | Description |
|---|---|---|
id | String! | ID of the consulting service to update |
input | AdminConsultingServiceUpdateInput! | Partial-update fields |
AdminConsultingServiceUpdateInput Fields
| Field | Type | Description |
|---|---|---|
name | String | Display name |
slug | String | URL slug |
description | String | Service description |
lecturerId | String | Lecturer ID; pass null to remove |
published | Boolean | Set to true to publish (auto-stamps publishedAt on first publish) |
tags | [String!] | Tag list. Replaces the entire list |
ratingFormId | String | Linked form ID; pass null to remove |
backgroundColor | String | UI background color (hex); pass null to remove |
Example
mutation UpdateConsultingService {
updateConsultingService(
id: "35a72dba-c3cc-4a06-b7eb-d784d010386e"
input: {
description: "Updated 45-minute career coaching session"
tags: ["career", "coaching", "premium"]
}
) {
consultingService {
id
name
description
tags
}
errors
}
}Common Errors
| Error code | Message |
|---|---|
CONSULTING-001 | Consulting service not found |
CONSULTING-003 | Slug must only contain lowercase letters, numbers, and hyphens |
CONSULTING-005 | Lecturer not found or not in this school |
CONSULTING-006 | Rating form not found or not in this school |
Delete a Consulting Service
The deleteConsultingService mutation soft-deletes (discards) a consulting service. Refuses to delete when the service still has undiscarded, non-canceled future meetings — cancel them via bulkCancelConsultingMeetings (or wait for them to end) first.
Input Parameters
| Field | Type | Description |
|---|---|---|
id | String! | ID of the consulting service to delete |
Example
mutation DeleteConsultingService {
deleteConsultingService(id: "35a72dba-c3cc-4a06-b7eb-d784d010386e") {
consultingService {
id
discardedAt
}
errors
}
}Common Errors
| Error code | Message |
|---|---|
CONSULTING-001 | Consulting service not found |
CONSULTING-004 | Cannot delete a consulting service that still has undiscarded, non-canceled future meetings. Cancel them first |
Bulk Create Consulting Meetings
The bulkCreateConsultingMeetings mutation creates multiple time slots under a single consulting service in one request. results[i] always corresponds to inputs[i].
⚠️
atomic: true+hostingType: zoomis rejected. Zoom rows provision against the Zoom API per-row, which makes a clean rollback impossible. The mutation top-level-fails withMEETING-010if you mix them. Leaveatomic: false(the default) whenever any row is Zoom-hosted — even an agent that defaults toatomic: true"for safety" will trip this on the first Zoom batch.
Time and Timezone
startedAt and endedAt are Unix timestamps (seconds since epoch). Convert from local datetimes using the parent service's effectiveTimezone field (IANA name, e.g. Asia/Taipei) before sending.
Input Parameters
| Field | Type | Description |
|---|---|---|
serviceId | String! | Parent consulting service ID |
inputs | [AdminConsultingMeetingBulkInput!]! | Time-slot rows |
atomic | Boolean | When true, all rows roll back if any row fails. Default false (per-row independent) |
AdminConsultingMeetingBulkInput Fields
Meeting host model — three independent concepts. A consulting meeting carries three different "host-ish" identifiers, and they are not interchangeable:
lecturerId— the public-facing Lecturer profile (slug, URL routing, slot-overlap detection). Managed in the school admin's Lecturers section. List via thelecturersquery.hostUserId— the user (school owner or teaching assistant) who actually runs the session. Surfaced to enrolled students. This is the "host" the school admin UI exposes on the meeting edit form. List valid hosts up-front viameetingHoststo avoidMEETING-012.hostEmail— the Zoom license email used to provision the Zoom room whenhostingType: zoom. A Zoom seat, not a person record. List valid seats up-front viazoomHoststo avoidMEETING-011.For a Zoom meeting you typically set all three:
lecturerId(whose profile owns the slot),hostUserId(who clicks "join" on the day), andhostEmail(which Zoom seat hosts the room). They can intentionally be three different people/seats.
| Field | Type | Required | Description |
|---|---|---|---|
startedAt | Int! | Yes | Slot start, Unix timestamp |
endedAt | Int! | Yes | Slot end, Unix timestamp |
lecturerId | String | No | Lecturer profile (slug, URL routing, slot-overlap detection). NOT the person who hosts the session — use hostUserId for that. Inherits the service-level lecturer when omitted |
hostUserId | String | No | The user who actually hosts the meeting. Must be the school owner's user id OR a user with the teaching_assistant role in this school; otherwise MEETING-012. Defaults to the school owner when omitted |
hostingType | MeetingHostingType | No | zoom, live_session, or custom |
hostingId | String | No | External hosting platform meeting ID (e.g. Zoom meeting ID) |
hostEmail | String | No | Zoom license email for hostingType: zoom — see Zoom host resolution below. Independent of hostUserId |
joinUrl | String | No | Pre-set join URL. Required when hostingType: custom |
maxAttendeeCapacity | Int | No | 0 or null means unlimited |
price | Float | No | Per-slot price; falls back to service-level pricing if omitted |
title | String | No | Falls back to the service name when omitted |
description | String | No |
Zoom host resolution
When hostingType: zoom:
- If
hostEmailis provided, it must match one of the school's configured Zoom integration licenses (Settings → Integrations → Zoom). Prefer listing them viazoomHostsup-front. - If omitted and the school has exactly one Zoom license, that license is used automatically.
- Otherwise, the row fails with
MEETING-011. The error string carries a machine-parseablevalid_options=email1,email2suffix so agents can recover without parsing prose.
On successful return for Zoom rows: the mutation provisions the Zoom meeting synchronously before responding, so meeting.hostingId (Zoom meeting ID) and meeting.joinUrl are populated by the time you read them in results[i].meeting. No follow-up query needed.
Return Type
type AdminBulkCreateConsultingMeetingsPayload {
results: [AdminConsultingMeetingBulkResult!]
allSucceeded: Boolean
errors: [String!]
}
type AdminConsultingMeetingBulkResult {
meeting: AdminConsultingMeeting # null when the row failed
errors: [String!] # null when the row succeeded
}Example
mutation BulkCreateConsultingMeetings {
bulkCreateConsultingMeetings(
serviceId: "35a72dba-c3cc-4a06-b7eb-d784d010386e"
atomic: false
inputs: [
{
startedAt: 1748390400
endedAt: 1748392200
hostingType: live_session
maxAttendeeCapacity: 1
}
{
startedAt: 1748394000
endedAt: 1748395800
hostingType: zoom
hostEmail: "host@example.com" # Zoom license seat
hostUserId: "9b8a7c65-1234-5678-90ab-cdef01234567" # the human who runs it (owner or TA)
maxAttendeeCapacity: 1
}
]
) {
allSucceeded
results {
meeting {
id
title
state
startedAt
endedAt
hostingType
joinUrl
}
errors
}
errors
}
}Common Errors (per row or top-level)
| Error code | Message |
|---|---|
CONSULTING-001 | Consulting service not found (top-level — bad serviceId) |
MEETING-008 | Custom hosting type requires joinUrl |
MEETING-009 | School has no active Zoom integration; configure Zoom under integrations first |
MEETING-010 | Zoom hosting type cannot be combined with atomic: true; use atomic: false to allow per-row Zoom provisioning |
MEETING-011 | hostEmail is required or invalid — error carries valid_options=email1,email2 suffix (capped at 20 entries; call zoomHosts for the full list) |
MEETING-012 | hostUserId must be the school owner or a teaching assistant — error carries valid_options=user_id_1,user_id_2 suffix (capped at 20 entries; call meetingHosts for the full list) |
Update a Consulting Meeting
The updateConsultingMeeting mutation applies a partial update. Time fields (startedAt, endedAt) are blocked when the meeting has enrolled students (see MEETING-003); remove students first, or cancel and recreate the slot, to reschedule. Other fields — including hostUserId, lecturerId, hostingType, title, maxAttendeeCapacity, etc. — are NOT blocked by MEETING-003 and can be changed on a scheduled meeting (e.g. to reassign hosts when staff turn over).
Explicit-null semantics: explicit
nullis treated as no change, not "clear". Sending{ lecturerId: null }keeps the existing lecturer. There is currently no API path to clear an already-assigned lecturer.
Input Parameters
| Field | Type | Description |
|---|---|---|
id | String! | Meeting ID |
input | AdminConsultingMeetingUpdateInput! | Partial-update fields |
AdminConsultingMeetingUpdateInput Fields
The three independent host concepts (
lecturerId/hostUserId/hostEmail) are explained in the bulk-create section above.
| Field | Type | Description |
|---|---|---|
startedAt | Int | Slot start, Unix timestamp |
endedAt | Int | Slot end, Unix timestamp |
lecturerId | String | Reassign Lecturer profile (not the actual host — see hostUserId) |
hostUserId | String | Reassign the user who runs the meeting (school owner or teaching assistant); MEETING-012 otherwise |
hostingType | MeetingHostingType | zoom, live_session, or custom |
hostingId | String | |
hostEmail | String | Zoom license email — same resolution rules as bulk-create |
joinUrl | String | |
maxAttendeeCapacity | Int | |
price | Float | |
title | String | |
description | String |
Return Type
type AdminUpdateConsultingMeetingPayload {
meeting: AdminConsultingMeeting
errors: [String!]
}Example
mutation UpdateConsultingMeeting {
updateConsultingMeeting(
id: "0186b768-c0c0-4d75-8fb7-7fee0b3ee9c1"
input: {
title: "Rescheduled coaching session"
maxAttendeeCapacity: 2
}
) {
meeting {
id
title
maxAttendeeCapacity
state
}
errors
}
}Common Errors
| Error code | Message |
|---|---|
MEETING-001 | Consulting meeting not found |
MEETING-003 | Cannot reschedule meeting with enrolled students |
MEETING-007 | Meeting is already canceled |
MEETING-008 | Custom hosting type requires joinUrl |
MEETING-009 | School has no active Zoom integration |
MEETING-011 | hostEmail is required or invalid — error carries valid_options=email1,email2 suffix (capped at 20 entries; call zoomHosts for the full list) |
MEETING-012 | hostUserId must be the school owner or a teaching assistant — error carries valid_options=user_id_1,user_id_2 suffix (capped at 20 entries; call meetingHosts for the full list) |
Cancel a Consulting Meeting
The cancelConsultingMeeting mutation cancels a single meeting via the underlying AASM state machine. Works from both available and scheduled states. For Zoom-hosted meetings, the remote Zoom meeting is also deleted; the local row keeps its hosting metadata for history.
Input Parameters
| Field | Type | Description |
|---|---|---|
id | String! | Meeting ID |
Example
mutation CancelConsultingMeeting {
cancelConsultingMeeting(id: "0186b768-c0c0-4d75-8fb7-7fee0b3ee9c1") {
meeting {
id
state
}
errors
}
}Common Errors
| Error code | Message |
|---|---|
MEETING-001 | Consulting meeting not found |
MEETING-007 | Meeting is already canceled |
Bulk Cancel Consulting Meetings
The bulkCancelConsultingMeetings mutation cancels multiple meetings in one round trip. results[i] always corresponds to ids[i]. Already-canceled meetings produce MEETING-007 in their per-row errors. For Zoom-hosted meetings, the remote Zoom meeting is also deleted (same behavior as the single cancelConsultingMeeting); the local rows keep their hosting metadata for history.
Input Parameters
| Field | Type | Description |
|---|---|---|
ids | [String!]! | Meeting IDs; order preserved in results |
atomic | Boolean | When true, all rows roll back if any row fails. Default false |
Example
mutation BulkCancelConsultingMeetings {
bulkCancelConsultingMeetings(
ids: ["meeting-id-a", "meeting-id-b"]
atomic: false
) {
allSucceeded
results {
meeting {
id
state
}
errors
}
}
}Common Errors
Same per-row codes as the single cancel: MEETING-001, MEETING-007. Top-level MEETING-010 if Zoom-hosted meetings are present and atomic: true.
Enroll a Student to a Consulting Meeting
The enrollStudentToConsultingMeeting mutation adds a student to a meeting. Provide either userId or email — one is required. Re-enrolling an already-enrolled student is a no-op and returns the existing meeting. An available meeting auto-transitions to scheduled on the first enrollment.
Scope: this mutation requires
students:write(ormembers:write), notcourses:write.
Input Parameters
| Field | Type | Required | Description |
|---|---|---|---|
meetingId | String! | Yes | Target meeting ID |
userId | String | No* | Existing student ID. Mutually exclusive with email |
email | String | No* | Student email; creates a school-scoped student if no match |
name | String | No | Required when creating a new user by email |
*Either userId or email must be provided.
Example
mutation EnrollStudent {
enrollStudentToConsultingMeeting(
meetingId: "0186b768-c0c0-4d75-8fb7-7fee0b3ee9c1"
email: "student@example.com"
name: "John Doe"
) {
meeting {
id
state
attendeeCount
}
user {
id
email
name
}
errors
}
}Common Errors
| Error code | Message |
|---|---|
MEETING-001 | Consulting meeting not found |
MEETING-004 | Meeting has reached its maximum attendee capacity |
MEETING-007 | Meeting is already canceled |
ENROLLMENT-001 | Either userId or email must be provided |
ENROLLMENT-002 | Student not found |
ENROLLMENT-005 | Failed to create student |
ENROLLMENT-006 | Invalid email |
ENROLLMENT-007 | Name is required when creating a new user |
Remove a Student from a Consulting Meeting
The removeStudentFromConsultingMeeting mutation removes a student's enrollment. If this leaves a scheduled meeting with zero attendees, the state auto-reverts to available so the slot is discoverable again via state filters. Refuses to operate on a canceled meeting (MEETING-007).
Scope: this mutation requires
students:write(ormembers:write).
Input Parameters
| Field | Type | Description |
|---|---|---|
meetingId | String! | Target meeting ID |
userId | String! | Student to remove |
Example
mutation RemoveStudent {
removeStudentFromConsultingMeeting(
meetingId: "0186b768-c0c0-4d75-8fb7-7fee0b3ee9c1"
userId: "387b0c87-0f85-44cc-833e-7306c1b5d3e6"
) {
meeting {
id
state
attendeeCount
}
errors
}
}Common Errors
| Error code | Message |
|---|---|
MEETING-001 | Consulting meeting not found |
MEETING-006 | Student is not enrolled in this meeting |
MEETING-007 | Meeting is already canceled |