This study was conducted within the Empowerment Agency Training (EAT) component of the SPHERES project.
This study aims to quantify and characterize health workers’ workload during antenatal care (ANC) services, focusing on time allocation between direct and indirect care activities, workload distribution across health workers and visit types, task concentration, and workflow fragmentation resulting from interruptions.
This study employed an expert shadowing observational design to document health workers’ work processes during antenatal care (ANC) service delivery. Activity- level data were collected through continuous, real-time observation of health workers during routine service hours at four public health centers (Puskesmas) in Purbalingga: Serayu Larangan (19 December 2025), Kaligondang (20 December 2025), Bojongsari (22 December 2025), and Pengadegan (23 December 2025).
In total, 9 midwives and 1 internship doctor were observed across the four facilities over four observation days. Each health worker was followed continuously throughout their working hours. All observed activities were recorded as discrete events with defined start and end times.
Activity-level data were recorded using Toggl Track, a digital time-tracking application.
| Intervention | Definition | Related activities |
|---|---|---|
| ANC Clinical Assessment | Systematic assessment of maternal and fetal condition conducted during antenatal care visits according to national ANC standards. |
|
| Mental Health Screening | Administration, scoring, interpretation, and communication of psychosocial or mental health screening tools conducted as part of ANC services. |
|
| Maternal Health Counseling and Education | Provision of information, advice, and education related to pregnancy, childbirth preparation, and maternal health. |
|
| Clinical Procedures | Performance of routine preventive or diagnostic procedures during ANC services. |
|
| Clinical Care Coordination / Case Discussion | Communication among midwives or with other health professionals aimed at clinical decision-making and continuity of care. |
|
| Supervision of Interns | Supervision, guidance, verification, and feedback related to ANC tasks performed by interns or students. |
|
| Documentation and Reporting | Recording, updating, and management of maternal health data in clinical or administrative systems. |
|
| Non-clinical Communication / Team Coordination | Communication related to workflow organization and service logistics, excluding clinical decision-making. |
|
| Supply and Equipment Management | Preparation, checking, and organization of materials and equipment required for ANC services. |
|
| Environmental Preparation | Preparation and maintenance of the ANC service environment to ensure safe and efficient care. |
|
| Walking / Movement | Physical movement between locations related to ANC service delivery. |
|
| Waiting Time | Time during which the health worker is unable to perform productive tasks due to system or process constraints. |
|
| System-Driven Non-ANC Clinical Care | Clinical services outside the formal ANC mandate but required by health system conditions. |
|
| Administrative / Non-ANC Tasks | Activities not directly related to ANC service delivery. |
|
Table 2 presents the key variables and their operational definitions used in the analysis. The dataset is structured at the activity level, with each record representing a discrete task performed by health workers during the expert shadowing observations. Variables include identifiers for observation timing and personnel, patient linkage where applicable, detailed classifications of service encounters and activities, as well as precise time stamps used to calculate activity duration.
| Variable | Description | Categories / Values |
|---|---|---|
| observation_date | The calendar date on which the expert shadowing observation was conducted. | Calendar date |
| worker_id | A unique identifier assigned to each observed healthcare provider. | Alphanumeric code |
| patient_id | An identifier for the patient associated with the observed activity. Recorded only for patient-related activities. | Alphanumeric code (may be missing for non-patient activities) |
| visit_type | The type of visit or service encounter during which the activity occurred. | ANC_K1_murni; ANC_K1_akses; ANC_K2; ANC_K3; ANC_ulangan; nifas; kunjungan_neonatus; catin; KB; kesehatan_reproduksi; ‘tidak ada’ indicates non-patient-related time |
| activity_type | A detailed coded representation of the specific task or action performed by health workers. | Multiple activity codes representing clinical, administrative, educational, coordination, and other tasks |
| activity_detail | A free-text description providing additional contextual information about the observed activity. | Free-text description |
| activity_category | A higher-level functional classification grouping activity types into analytically meaningful categories. | pelayanan (direct clinical care); edukasi (health education); administrasi (documentation and reporting); koordinasi (team communication); pra_layanan (service preparation); menunggu (waiting time); interupsi (interruptions); non_pelayanan (non-clinical activities) |
| start_time | The recorded start time of the observed activity. | HH:MM:SS |
| end_time | The recorded end time of the observed activity. | HH:MM:SS |
| duration_seconds | The duration of the activity, calculated as the time difference between end time and start time, expressed in seconds. | Numeric (seconds) |
| Total observed activities | Total observation time (hours) | Number of health workers | Number of public health centers | Number of observation days |
|---|---|---|---|---|
| 187 | 5.96 | 10 | 4 | 4 |
# Color Palette
palette_ss <- c(
turmeric = "#E6B800",
dark_purple = "#6E5A8C",
aquamarine = "#66C5CC",
muted_teal = "#4C8C8A",
warm_gray = "#C7BEBE",
deep_slate = "#2C2C34"
)
my_palette <- palette_ss
# Care Level Colors
care_level_colors <- c(
"Direct care" = palette_ss["aquamarine"],
"Indirect care" = palette_ss["muted_teal"],
"Interruption" = palette_ss["turmeric"],
"Non-care" = palette_ss["warm_gray"]
)
# Report Theme
theme_ss_report <- function(base_size = 12, base_family = "") {
theme_minimal(base_size = base_size, base_family = base_family) +
theme(
text = element_text(color = "black"),
plot.title = element_text(
face = "bold",
hjust = 0.5,
size = base_size + 1
),
plot.subtitle = element_text(
hjust = 0.5,
size = base_size - 1
),
axis.title = element_text(face = "bold"),
panel.grid.major = element_line(color = "#EAEAEA"),
panel.grid.minor = element_blank(),
legend.title = element_text(face = "bold"),
legend.position = "bottom",
plot.margin = margin(10, 10, 10, 10)
)
}
# Scale Helpers
scale_fill_care <- function(...) {
scale_fill_manual(
values = unname(care_level_colors),
...
)
}
scale_color_care <- function(...) {
scale_color_manual(
values = unname(care_level_colors),
...
)
}
Time allocation analysis shows how observed working time was
distributed between direct care and indirect care activities across
public health centers and individual healthcare workers.
Puskesmas Kaligondang shows the lowest proportion of indirect care and the highest share of direct care, indicating a stronger focus on patient-facing activities. In contrast, Puskesmas Pengadegan allocates a larger proportion of time to indirect care, with direct care comprising only a small portion of the total observed time. These differences likely reflect variations in workflow organization, administrative burden, patient flow, or task delegation practices across the health centers.
At Puskesmas Pengadegan, all observed workers allocate the majority of their time to indirect care, with relatively limited variation between individuals, suggesting a consistently high non-clinical workload across the staff. In contrast, Puskesmas Kaligondang shows clear heterogeneity: one worker allocates the majority of their observed time to direct care, while others spend more time on indirect activities. This indicates differences in role assignment, patient load, or task specialization among workers within the same facility.
Variation is also evident in Puskesmas Bojongsari, where some workers show a higher proportion of indirect care compared to their peers. Puskesmas Serayu Larangan, although represented by fewer observed workers, shows a more balanced pattern, yet still dominated by indirect care.
These findings indicate that the allocation of time between direct care and indirect care varies not only across different health centers but also among individual workers within the same facility, reflecting differences in their roles, task assignments, and the nature of their work.
# ------------------------------------------------------------
# Create unique worker identifier (PHC × worker)
# ------------------------------------------------------------
data_clean <- data_clean %>%
mutate(
worker_uid = paste(phc_id, worker_id, sep = "_"),
worker_label = paste(phc_id, worker_id, sep = "\n")
)
# ------------------------------------------------------------
# Aggregate direct and indirect care by healthcare worker
# ------------------------------------------------------------
care_by_worker <- data_clean %>%
filter(care_level %in% c("Direct care", "Indirect care")) %>%
group_by(phc_id, worker_id, care_level) %>%
summarise(
total_seconds = sum(duration_seconds),
.groups = "drop"
) %>%
group_by(phc_id, worker_id) %>%
mutate(
proportion = total_seconds / sum(total_seconds)
)
# ------------------------------------------------------------
# Visualization: Direct vs indirect care by healthcare worker
# ------------------------------------------------------------
ggplot(
care_by_worker,
aes(
x = worker_id,
y = proportion,
fill = care_level
)
) +
geom_col(width = 0.7) +
facet_wrap(~ phc_id, nrow = 1) +
scale_fill_care() +
scale_y_continuous(labels = scales::percent) +
labs(
title = "Direct vs Indirect Care by Healthcare Worker",
subtitle = "Workers nested within public health centers",
x = "Healthcare worker",
y = "Proportion of observed time",
fill = "Care level"
) +
theme_ss_report() +
theme(
strip.text = element_text(face = "bold"),
axis.text.x = element_text(size = 9)
)
At Puskesmas Bojongsari, the observed workload is relatively evenly distributed across the healthcare workers, with each individual contributing a comparable proportion of the total observed time. This pattern suggests a balanced allocation of responsibilities within the facility. In contrast, Puskesmas Pengadegan shows a highly unequal workload distribution, with one healthcare worker taking on a disproportionately large share of the observed working time compared to others. This may indicate role concentration, a higher workload for certain workers, or task centralization within the facility.
At Puskesmas Kaligondang, the workload distribution is more varied, with a few workers contributing significantly higher shares of total observed time. This suggests some imbalance in task distribution, possibly due to differences in assigned duties, specialized roles, or patient demand.
# ------------------------------------------------------------
# Aggregate total observed workload by healthcare worker
# ------------------------------------------------------------
workload_by_worker <- data_clean %>%
group_by(phc_id, worker_id) %>%
summarise(
total_seconds = sum(duration_seconds),
.groups = "drop"
)
# ------------------------------------------------------------
# Calculate workload share within each PHC
# ------------------------------------------------------------
workload_share <- workload_by_worker %>%
group_by(phc_id) %>%
mutate(
workload_share = total_seconds / sum(total_seconds)
)
# ------------------------------------------------------------
# Visualization: Workload distribution by worker (within PHC)
# ------------------------------------------------------------
ggplot(
workload_share,
aes(
x = worker_id,
y = workload_share
)
) +
geom_col(
width = 0.7,
fill = palette_ss["dark_purple"]
) +
scale_y_continuous(labels = scales::percent) +
facet_wrap(~ phc_id, nrow = 1) +
labs(
title = "Distribution of Observed Workload Across Health Workers",
subtitle = "Share of total observed working time within each public health center",
x = "Healthcare worker",
y = "Share of total workload"
) +
theme_ss_report() +
theme(
axis.text.x = element_text(size = 9),
strip.text = element_text(face = "bold")
)
Puskesmas Serayu Larangan displays a more moderate workload
distribution, though variability still exists across healthcare workers.
While no single worker dominates, the distribution indicates some
imbalance, likely reflecting differences in task assignment or the
number of patients handled by each worker.
These differences in workload distribution across the puskesmas may reflect variations in how tasks are assigned, the number of patients seen by each worker, and how roles are structured, all of which could influence both efficiency and the overall work-life balance within the facility.
# ------------------------------------------------------------
# Workload inequality metric: Coefficient of Variation (CV)
# ------------------------------------------------------------
workload_inequality <- workload_by_worker %>%
group_by(phc_id) %>%
summarise(
mean_seconds = mean(total_seconds),
sd_seconds = sd(total_seconds),
cv = sd_seconds / mean_seconds,
.groups = "drop"
)
# ------------------------------------------------------------
# Visualization: Workload share by health workers (within PHC)
# ------------------------------------------------------------
ggplot(
workload_share,
aes(
x = workload_share,
y = reorder(worker_id, workload_share)
)
) +
geom_segment(
aes(
xend = workload_share,
yend = worker_id
),
x = 0,
color = palette_ss["warm_gray"]
) +
geom_point(
size = 3.5,
color = palette_ss["dark_purple"]
) +
scale_x_continuous(
labels = scales::percent,
) +
facet_wrap(~ phc_id, nrow = 1, scales = "free_y") +
labs(
title = "Observed Workload Share by Health Horkers",
subtitle = "Each panel represents relative workload distribution within a public health center",
x = "Share of total observed workload",
y = "Healthcare worker"
) +
theme_ss_report()
The figure presents the relative distribution of observed workload
across healthcare workers within each public health center, summarized
using the coefficient of variation (CV) as a measure of workload
dispersion. The CV captures how much individual workloads deviate from
the mean workload within each facility, providing a way to compare the
inequality in workload distribution across different centers regardless
of their total workload volume.
At Puskesmas Bojongsari, the workload distribution among healthcare workers is relatively even, with minimal variation in the share of total observed workload. This indicates a low coefficient of variation, suggesting that task allocation is well-balanced and evenly distributed among the workers.
In contrast, Puskesmas Kaligondang shows a slightly greater spread in workload distribution. While some healthcare workers share a more balanced workload, there is a noticeable difference between workers, indicating a moderate level of variation and a higher CV. This suggests that, while relatively balanced, there may be some uneven distribution of tasks within the facility.
At Puskesmas Pengadegan, the workload is more concentrated, with one healthcare worker accounting for a significantly larger share of the total workload. This results in a higher CV, indicating greater inequality in workload distribution among the staff.
Finally, Puskesmas Serayu Larangan also shows a moderate degree of workload dispersion, similar to Puskesmas Kaligondang, with some imbalance but no extreme concentration. The CV for this center suggests that the workload is more evenly distributed than in Puskesmas Pengadegan, though still not perfectly balanced.
The findings indicate that specific task categories are concentrated among certain healthcare workers, with varying levels of concentration across public health centers. At Puskesmas Bojongsari and Puskesmas Kaligondang, some workers allocate a significant portion of their time to specialized tasks such as clinical service delivery and administrative duties, indicating task specialization. In contrast, Puskesmas Pengadegan and Puskesmas Serayu Larangan show a more even distribution of tasks, with fewer extreme differences in time allocation across workers, suggesting a more generalized role assignment. These patterns highlight that while some facilities have more specialized roles, others maintain a broader distribution of tasks among their staff, leading to uneven task concentration across different health centers.
# ------------------------------------------------------------
# Aggregate time by worker and activity category
# ------------------------------------------------------------
task_by_worker <- data_clean %>%
group_by(phc_id, worker_id, activity_category) %>%
summarise(
total_seconds = sum(duration_seconds),
.groups = "drop"
)
# ------------------------------------------------------------
# Compute within-worker task composition (share)
# ------------------------------------------------------------
task_share_worker <- task_by_worker %>%
group_by(phc_id, worker_id) %>%
mutate(
share = total_seconds / sum(total_seconds)
) %>%
ungroup()
# ------------------------------------------------------------
# Visualization 1: Task composition by health workers (stacked bars)
# ------------------------------------------------------------
ggplot(
task_share_worker,
aes(
x = worker_id,
y = share,
fill = activity_category
)
) +
geom_col(width = 0.7) +
scale_y_continuous(labels = scales::percent) +
facet_wrap(~ phc_id, nrow = 1) +
labs(
title = "Task Composition by Health Workers",
subtitle = "Proportion of observed working time by activity category",
x = "Healthcare worker",
y = "Share of total working time",
fill = "Activity category"
) +
theme_ss_report() +
theme(
axis.text.x = element_text(size = 9),
strip.text = element_text(face = "bold")
)
# ------------------------------------------------------------
# Visualization 2: Task specialization patterns (heatmap)
# ------------------------------------------------------------
ggplot(
task_share_worker,
aes(
x = activity_category,
y = worker_id,
fill = share
)
) +
geom_tile(color = "white") +
scale_fill_viridis_c(
labels = scales::percent,
option = "C"
) +
facet_wrap(~ phc_id) +
labs(
title = "Task Specialization Patterns Across Health Workers",
subtitle = "Relative time allocation by activity category within each facility",
x = "Activity category",
y = "Healthcare worker",
fill = "Time share"
) +
theme_ss_report() +
theme(
axis.text.x = element_text(angle = 30, hjust = 1)
)
library(dplyr)
library(stringr)
# ------------------------------------------------------------
# Clean visit_type
# ------------------------------------------------------------
visit_clean <- data_clean %>%
filter(
care_level %in% c("Direct care", "Indirect care"),
!is.na(visit_type)
) %>%
mutate(
visit_type = visit_type %>%
str_trim() %>%
str_to_lower()
) %>%
filter(visit_type != "tidak ada") %>%
mutate(
visit_type = case_when(
visit_type == "anc_k1_murni" ~ "ANC_K1_murni",
visit_type == "anc_k1_akses" ~ "ANC_K1_akses",
visit_type == "anc_ulangan" ~ "ANC_ulangan",
visit_type == "pnc" ~ "PNC",
visit_type == "kb" ~ "KB",
visit_type == "anak" ~ "Anak",
TRUE ~ NA_character_
)
) %>%
filter(!is.na(visit_type))
# ------------------------------------------------------------
# Aggregate & calculate proportions
# ------------------------------------------------------------
time_prop_by_visit <- visit_clean %>%
group_by(visit_type, care_level) %>%
summarise(
total_seconds = sum(duration_seconds, na.rm = TRUE),
.groups = "drop"
) %>%
group_by(visit_type) %>%
mutate(
proportion = total_seconds / sum(total_seconds)
)
# ------------------------------------------------------------
# Visualization: stacked proportion bar
# ------------------------------------------------------------
ggplot(
time_prop_by_visit,
aes(
x = visit_type,
y = proportion,
fill = care_level
)
) +
geom_col(
width = 0.65,
color = "white",
linewidth = 0.3
) +
scale_fill_care() +
scale_y_continuous(
labels = scales::percent_format(accuracy = 1),
expand = c(0, 0)
) +
scale_x_discrete(
labels = function(x) {
stringr::str_replace_all(x, "_", "\n") %>%
stringr::str_to_upper()
}
) +
labs(
title = "Proportion of Direct and Indirect Care by Visit Type",
subtitle = "Distribution of observed working time within each visit type",
x = "Visit type",
y = "Proportion of time",
fill = "Care level"
) +
theme_ss_report() +
theme(
axis.text.x = element_text(
angle = 90,
size = 6,
hjust = 1,
vjust = 0.5
)
)
# ------------------------------------------------------------
# Direct vs Indirect Care Time Allocation
# ------------------------------------------------------------
library(dplyr)
library(stringr)
library(ggplot2)
visit_duration <- visit_clean %>%
filter(
!is.na(visit_type),
visit_type != "tidak ada"
) %>%
group_by(visit_type, patient_id) %>% # satu visit episode
summarise(
visit_minutes = sum(duration_seconds, na.rm = TRUE) / 60,
.groups = "drop"
)
time_by_visit <- visit_duration %>%
mutate(
visit_type = visit_type %>%
str_trim() %>%
str_to_lower() %>%
recode(
"anc_k1_murni" = "ANC_K1_murni",
"anc_ulangan" = "ANC_ulangan",
"pnc" = "PNC",
"kb" = "KB",
"anak" = "Anak",
.default = NA_character_
)
) %>%
filter(!is.na(visit_type)) %>%
group_by(visit_type) %>%
summarise(
mean_minutes = mean(visit_minutes),
median_minutes = median(visit_minutes),
n_visits = n(),
.groups = "drop"
)
# ------------------------------------------------------------
# Visualization: Stacked proportional bar chart
# ------------------------------------------------------------
ggplot(
time_by_visit,
aes(
x = visit_type,
y = mean_minutes
)
) +
geom_col(
width = 0.6,
fill = palette_ss["muted_teal"]
) +
labs(
title = "Average Service Time per Visit Type",
subtitle = "Mean duration per visit episode",
x = "Visit type",
y = "Average time per visit (minutes)"
) +
scale_x_discrete(
labels = function(x) str_replace_all(x, "_", "\n")
) +
theme_ss_report() +
theme(
axis.text.x = element_text(
angle = 90,
size = 7,
hjust = 1
)
)
# ------------------------------------------------------------
# Overall interruption summary (descriptive)
# ------------------------------------------------------------
interrupt_overall <- data_clean %>%
summarise(
total_seconds = sum(duration_seconds),
interruption_seconds = sum(duration_seconds[care_level == "Interruption"]),
interruption_share = interruption_seconds / total_seconds
)
interrupt_overall
## # A tibble: 1 × 3
## total_seconds interruption_seconds interruption_share
## <dbl> <dbl> <dbl>
## 1 21449 NA NA
# ------------------------------------------------------------
# Aggregate interruption, direct, and indirect care by PHC
# ------------------------------------------------------------
interrupt_by_phc <- data_clean %>%
filter(
care_level %in% c("Direct care", "Indirect care", "Interruption")
) %>%
group_by(phc_id, care_level) %>%
summarise(
total_seconds = sum(duration_seconds),
.groups = "drop"
) %>%
group_by(phc_id) %>%
mutate(
proportion = total_seconds / sum(total_seconds)
) %>%
ungroup()
# ------------------------------------------------------------
# Visualization: Proportion of time affected by interruptions
# ------------------------------------------------------------
ggplot(
interrupt_by_phc,
aes(
x = phc_id,
y = proportion,
fill = care_level
)
) +
geom_col(width = 0.7) +
scale_fill_care() +
scale_y_continuous(labels = scales::percent) +
labs(
title = "Proportion of Time Affected by Interruptions Across PHCs",
subtitle = "Distribution of direct care, indirect care, and interruption time",
x = "Public health center",
y = "Proportion of observed time",
fill = "Care level"
) +
theme_ss_report()