hours, signed_up_at, checked_in_at, checked_out_at, and the linkage to a Project Date. But there’s no GET /participations endpoint to pull it directly. Hours reporting requires walking either users (via /users/{id} → embedded participations) or Project Dates (via /projects/date/{id} → embedded participants).
This recipe walks through both approaches: when each is right, how to aggregate efficiently, how to build incremental reporting that doesn’t re-scan everything every time, and the performance patterns for large datasets.
What you’ll build
A reporting pipeline that:- Collects all volunteer participation data despite no direct participations endpoint
- Aggregates hours by user, project, organization, time period, and other dimensions
- Builds incremental reports that update rather than fully recompute
- Stores aggregated results for fast querying by downstream BI tools or dashboards
- Surfaces hours-related insights (trends, top volunteers, project performance)
When this recipe fits
The data flow
Five components:
The raw store is the integration’s canonical record of participations — once collected, aggregation is cheap and re-aggregation for different reports is straightforward.
Two collection strategies
The fundamental question: how do you collect participation records when there’s no direct participations endpoint?Strategy A: walk Project Dates
For each upcoming or recent Project Date, fetch its participants:JavaScript
Strategy B: walk Users
For each User, fetch their detail (which embedsparticipations):
JavaScript
Which to use
For most production reporting, Strategy A is the default. It’s cheaper, the data captured is richer (includes Project name), and Project-Date-based collection aligns naturally with reporting dimensions.
Step 1: incremental collection
Re-scanning the entire dataset on every refresh is wasteful. Build incremental collection:JavaScript
Raw store schema
project_date_id, user_id) is the natural unique identifier for a participation. Indexes by user, by project, and by time support the common aggregation queries.
Step 2: aggregate hours
Once raw participations are in the local store, aggregation is fast:Hours per user
Hours per project
Hours per time period
Step 3: pre-computed aggregations
For high-traffic dashboards, even fast SQL queries can be slow at scale. Materialize aggregations:JavaScript
Step 4: report-specific patterns
Different reports need different shapes. A few common ones:Top volunteers ranking
JavaScript
Volunteer engagement trend
JavaScript
Retention cohort analysis
JavaScript
Hours-by-organization (within the org family)
JavaScript
Step 5: handle unverified vs. verified hours
Participations have averified flag — true when the organizer confirmed attendance, false for self-reported or pending. Reports typically distinguish:
For most reports, verified-only is the canonical metric. Make the distinction explicit in dashboard labels: “Total verified hours: 2,450” rather than just “Total hours.”
Step 6: reconciliation and accuracy
Reports about hours need to be accurate — undercounting embarrasses the customer; overcounting embarrasses the integration.Daily verification scan
JavaScript
Verified vs. final-version drift
Sometimes a participation’shours field changes after initial capture — organizers may adjust hours post-event. The incremental upsert handles this automatically (new value overwrites old), but you may want to track when changes happen:
JavaScript
Performance at scale
A few patterns that help reports scale:Partition the raw store
For customers with millions of participations, partition by time:Tiered aggregation
For dashboards needing fast responses:
The dashboard hits the materialized view at the appropriate granularity; the raw data remains for ad-hoc queries.
Pre-computed user lookups
For per-user dashboards (e.g., “show me my volunteer history”), pre-compute per-user summaries:Things to watch for
Hours field is a string
Thehours field is returned as a string ("4.00") per audit #40. Always parse:
JavaScript
|| 0 defends against null or invalid values.
Participation modifications happen post-event
Organizers commonly adjust hours, verify participations, and add notes after a shift ends. The incremental collector picks these up because Project Dates’ participants can change. But don’t assume yesterday’s data is final — the collector should keep re-scanning recent Project Dates (e.g., last 14 days) for changes.Unverified participations are noisy
verified: false participations are often self-reported or pending — including them in reports can produce inflated numbers. Default to verified: true only for customer-facing reports.
Project deletion can orphan participations
If a Project is deleted in VOMO (admin action), its Project Dates and embedded participations disappear. Your local store still has them — they’ll appear in historical reports but not in current VOMO state. Decide whether to keep them (historical accuracy) or remove them (current accuracy). Typically: keep them with avomo_deleted_at timestamp, so historical reports remain accurate but current state shows “this Project no longer exists in VOMO.”
Multiple participations per user per Date are unusual but possible
The primary key(project_date_id, user_id) enforces one row per user per Date. If VOMO ever returns multiple participation entries for the same (user, date) tuple, the upsert will collapse them into one. This is almost certainly the right behavior, but be aware of it for reports that involve participation counts.
What you’ve built
After this recipe:- ✅ A collector that pulls participation records from VOMO via Project Dates
- ✅ A local raw store keyed by
(project_date_id, user_id) - ✅ Incremental collection that doesn’t re-scan everything every run
- ✅ Aggregated views for the common reporting questions
- ✅ Pre-computed materialized views for fast dashboards
- ✅ Daily reconciliation catching gaps
- ✅ Change tracking for hours modifications
Where to go next
Combine Volunteer Data with CRM+ Data
The cross-API recipe joining Volunteer hours with CRM+ donor data.
Build a Volunteer Self-Service Portal
The end-user-facing recipe using participation data.
The Volunteer Data Model
The data model context for participations.
Reconciliation Patterns
The reconciliation patterns this recipe uses.