Skip to main content

Staff IDs Architecture

This document explains how staff assignment works throughout WooCommerce Appointments—from data models to REST APIs to database storage.

Key Principle

Appointments support multiple staff members. The canonical storage is always staff_ids (plural, array). The singular staff_id is a convenience accessor or filter parameter.

Quick Reference

ContextSingular (staff_id)Plural (staff_ids)
WC_Appointment classget_staff_id() returns first staff (int)get_staff_ids() returns all staff (array)
REST API query paramsFilter by single staffFilter by multiple staff (comma-separated)
REST API responsePer-slot staff assignmentAppointment's full staff array
DatabaseN/A_appointment_staff_id meta (multiple entries)

WC_Appointment Class

Getting Staff

// Get ALL assigned staff as an array (preferred)
$staff_ids = $appointment->get_staff_ids();
// Returns: array( 123, 456 ) or array() if no staff

// Get the first/primary staff ID (convenience method)
$staff_id = $appointment->get_staff_id();
// Returns: int (first staff ID, or 0 if no staff)

// Get staff member objects with names
$staff_members = $appointment->get_staff_members(); // Array of WC_Product_Appointment_Staff
$staff_names = $appointment->get_staff_members( true ); // Comma-separated names string
$staff_links = $appointment->get_staff_members( false, true ); // HTML links

Setting Staff

// Set single staff member
$appointment->set_staff_id( 123 );
// Internally stores: staff_ids = [123]

// Set multiple staff members
$appointment->set_staff_ids( array( 123, 456 ) );
// Internally stores: staff_ids = [123, 456]

// IMPORTANT: Both methods write to the 'staff_ids' property
// There is NO separate 'staff_id' storage in the data model

Common Patterns

// Check if any staff assigned
if ( $appointment->get_staff_ids() ) {
// Has staff
}

// Check for specific staff
if ( in_array( 5, $appointment->get_staff_ids() ) ) {
// Staff ID 5 is assigned
}

// Get staff count
$staff_count = count( $appointment->get_staff_ids() );

REST API

Appointments Endpoint

Query Parameters:

  • staff_id — Filter appointments by a single staff member

Response includes:

  • staff_ids — Array of all assigned staff IDs
  • staff_details — Array of staff objects with id, display_name, avatar_url
# Filter appointments by staff member
curl -u admin:APP_PASSWORD \
"https://example.com/wp-json/wc-appointments/v2/appointments?staff_id=5"

Response:

{
"id": 123,
"staff_ids": [5, 8],
"staff_details": [
{
"id": 5,
"display_name": "John Smith",
"avatar_url": "..."
},
{
"id": 8,
"display_name": "Jane Doe",
"avatar_url": "..."
}
]
}

Slots Endpoint

The slots endpoint returns availability per staff member. Each slot record has a single staff_id representing which staff member that slot is for.

Query Parameters (both accepted, converted to array internally):

  • staff_id — Single staff ID (e.g., staff_id=5)
  • staff_ids — Comma-separated staff IDs (e.g., staff_ids=5,8,12)

Response:

  • Each slot record has staff_id (int) — the staff member for that specific slot
# Get slots for specific staff members
curl -u admin:APP_PASSWORD \
"https://example.com/wp-json/wc-appointments/v2/slots?product_id=123&staff_ids=5,8&min_date=2025-12-01&max_date=2025-12-31"

Response:

{
"records": [
{
"product_id": 123,
"product_name": "Consultation",
"date": "2025-12-01T10:00",
"timestamp": 1764547200,
"duration": 60,
"duration_unit": "minute",
"available": 1,
"scheduled": 0,
"staff_id": 5
},
{
"product_id": 123,
"product_name": "Consultation",
"date": "2025-12-01T10:00",
"timestamp": 1764547200,
"duration": 60,
"duration_unit": "minute",
"available": 1,
"scheduled": 0,
"staff_id": 8
}
],
"count": 2
}

Combined Staff Mode

Use combine_staff=true to get a single slot per time with summed availability across staff:

curl "https://example.com/wp-json/wc-appointments/v2/slots?product_id=123&combine_staff=true&min_date=2025-12-01"

Response:

{
"records": [
{
"product_id": 123,
"product_name": "Consultation",
"date": "2025-12-01T10:00",
"timestamp": 1764547200,
"duration": 60,
"duration_unit": "minute",
"available": 2,
"scheduled": 0,
"staff_id": 0
}
],
"count": 1
}

When combine_staff=true, staff_id is 0 because availability is summed across multiple staff.

Index Endpoint

Query Parameters:

  • staff_id — Filter indexed appointments/availability by staff
curl -u admin:APP_PASSWORD \
"https://example.com/wp-json/wc-appointments/v2/index?start_ts=1764547200&end_ts=1764719999&staff_id=5"

Availabilities Endpoint

Query Parameters:

  • staff_id — Filter availability rules by staff

Database Storage

Meta Key

Staff IDs are stored in WordPress post meta with key _appointment_staff_id.

Single staff appointment:

INSERT INTO wp_postmeta (post_id, meta_key, meta_value) 
VALUES (100, '_appointment_staff_id', 5);

Multi-staff appointment (multiple entries with same key):

INSERT INTO wp_postmeta (post_id, meta_key, meta_value) VALUES (101, '_appointment_staff_id', 5);
INSERT INTO wp_postmeta (post_id, meta_key, meta_value) VALUES (101, '_appointment_staff_id', 8);

Data Store Mapping

In WC_Appointment_Data_Store:

private array $appointment_meta_key_to_props = [
'_appointment_staff_id' => 'staff_ids', // Note: meta key is singular, prop is plural
// ...
];

The data store reads all values with get_post_meta($id, '_appointment_staff_id', false) (the false returns an array of all values).

Querying by Staff

// Get appointments for a specific staff member in a date range
$appointments = WC_Appointment_Data_Store::get_appointments_in_date_range(
$start_timestamp,
$end_timestamp,
$product_id, // 0 for all products
$staff_id // Staff ID to filter by
);

Product Staff Configuration

Products can have different staff assignment modes:

ModeValueDescriptionstaff_ids Behavior
Customer selectscustomerCustomer picks staff at bookingSingle staff ID chosen by customer
AutomaticautomaticSystem auto-assigns available staffSingle staff ID assigned by system
All staff requiredallAll assigned staff are booked togetherMultiple staff IDs (all assigned staff)
// Check product's staff assignment mode
$mode = $product->get_staff_assignment();

// Get product's available staff
$product_staff_ids = $product->get_staff_ids();

if ( 'all' === $mode ) {
// All staff will be assigned to each appointment
}

Availability Cache

The availability cache (WC_Appointments_Availability_Cache) uses a separate staff_id property for filtering cached entries. This is distinct from the appointment's staff_ids array.

// Cache entry has single staff_id for filtering
$cache_entry->get_staff_id(); // Returns int
$cache_entry->set_staff_id( 5 );

Best Practices

DO

  • ✅ Use get_staff_ids() to get all assigned staff
  • ✅ Use get_staff_id() only when you know there's a single staff
  • ✅ Use set_staff_ids() with an array for multi-staff
  • ✅ Check for empty array when verifying no staff assigned
  • ✅ Use staff_ids (comma-separated) in API calls when filtering multiple staff

DON'T

  • ❌ Don't assume only one staff member is assigned
  • ❌ Don't access $appointment->data['staff_id'] directly (doesn't exist)
  • ❌ Don't store staff in a separate staff_id property

Test Fixture Example

// Single staff
$appointment = $this->create_test_appointment([
'product_id' => $product_id,
'staff_id' => 5, // Convenience - stored in staff_ids
'start' => $start,
'end' => $end,
]);

// Multiple staff (preferred for multi-staff scenarios)
$appointment = $this->create_test_appointment([
'product_id' => $product_id,
'staff_ids' => [5, 8, 12], // Explicit array
'start' => $start,
'end' => $end,
]);

Troubleshooting

"Staff not saved" Issues

// WRONG - this won't persist
$appointment->staff_ids = array( 5, 8 );

// CORRECT - use the setter
$appointment->set_staff_ids( array( 5, 8 ) );
$appointment->save();

"get_staff_id() returns 0"

This happens when:

  1. No staff is assigned
  2. Staff IDs array is empty
$staff_ids = $appointment->get_staff_ids();
if ( empty( $staff_ids ) ) {
// No staff assigned - this is expected for some products
}

API Returns Empty Staff

If the slots API returns staff_id: 0:

  • Product may not have staff assigned
  • Using combine_staff=true mode (summed availability)
  • Staff filter doesn't match product's available staff