What you’ll build
A pattern for query-driven Groups that:- Accepts a query specification (criteria for membership)
- Resolves the criteria to a set of VOMO User IDs
- Creates a Group populated with those Users (or updates an existing one)
- Maintains the Group over time via scheduled refreshes
- Tracks what changed between refreshes (audit + monitoring)
- Handles failure cases (deleted users, criteria changes, API failures)
When this recipe fits
The fundamental tension: snapshot vs. dynamic
Groups in VOMO are persistent collections — once you set members, those members stay until you explicitly change them. They’re not dynamic queries that re-evaluate on read. This means a “Group from a query” pattern has to choose:
For most production integrations, periodically refreshed is the right pattern. Snapshots become stale; event-driven is hard to keep correct as edge cases accumulate.
The dynamic-query trap
A common request from customers: “Can we have a Group that automatically includes anyone who serves in 2025?” The answer: yes, but it’s a Group that you maintain on a schedule — not a Group that VOMO automatically updates. The integration’s value-add is the schedule + criteria evaluation. The customer sees “the Group is always current,” but under the hood your integration is doing periodic re-population. Set this expectation explicitly. A “self-updating Group” implies the integration is in the loop, not that VOMO has dynamic group functionality.Architecture
Five components:Step 1: define the query specification
Each query-driven Group has a stored specification:JavaScript
JavaScript
Step 2: resolve the criteria to User IDs
JavaScript
Resolver: “served during a time window”
The most common — and hardest, since participations aren’t directly queryable. Walk active Projects and check each Project Date’s participants:JavaScript
Resolver: “users created after a date”
Much cheaper — single filter on/users:
JavaScript
Resolver: “email domain”
JavaScript
Resolver: “has certificate”
The earned-certificate data shape on
UserDetailResource is undocumented in the OpenAPI spec. The resolver below assumes an earned_certificates array on the detail response. Confirm against live data before relying on it.JavaScript
Resolver: “external roster”
JavaScript
Resolver: combined criteria
JavaScript
Step 3: create or update the Group
JavaScript
Step 4: schedule the refresh
JavaScript
Right-sizing cadence by criteria type
Match cadence to how often the underlying data actually changes. Hourly refresh of “users with @wayne.example email” is wasteful.
Step 5: customer-facing visibility
JavaScript
Common queries with full specifications
Volunteers who served in the last 90 days
JavaScript
startDate re-evaluates at each refresh — the window moves forward each day.
First-time volunteers in 2025
JavaScript
External HR roster sync
JavaScript
Handling failures
Resolver failures
JavaScript
Detecting deleted users in resolved sets
If the resolver returns User IDs that no longer exist in VOMO, the PUT to Group members will fail validation. Filter before pushing:JavaScript
Things to watch for
The resolver determines the cost
“users created after 2024-01-01” = one paginated query. “served during 2024” = many Project Date fetches. Choose the criteria type with the cost in mind.Member churn signals issues
If a Group’s member count swings dramatically between refreshes (247 yesterday, 12 today), something is wrong:- The query criteria changed unexpectedly
- The underlying data changed (mass deletion, organization restructure)
- The resolver has a bug
Group hierarchy considerations
If the target Group has aparent_id (is part of a hierarchy), the refresh shouldn’t disturb that. The PUT to /groups/{id}/members replaces only the member list — but PUT to /groups/{id} (for metadata) requires preserving parent_id. Use the GET-then-PUT pattern.
Email-based external rosters and the email-change problem
For external-roster criteria, email matches users. If an email changes externally but not in VOMO (or vice versa), the user may disappear from the Group on the next refresh. Track external IDs separately from emails. See The email-change problem.Rate-limit pressure during resolution
Resolvers likeserved_during make many API calls. For partner integrations refreshing many specs simultaneously, stagger the schedule:
JavaScript
What you’ve built
After this recipe:- ✅ A query specification model (criteria + target Group + cadence)
- ✅ Resolver functions for common criteria types
- ✅ A refresh pipeline that creates/updates Groups idempotently
- ✅ A diff calculator producing per-refresh audit data
- ✅ Scheduled refresh with right-sized cadences
- ✅ Customer-facing visibility into refresh history
- ✅ Failure handling that surfaces problems for review
Where to go next
Report on Volunteer Hours
The reporting recipe using participation data.
Combine Volunteer Data with CRM+ Data
The cross-API recipe stitching Volunteer with CRM+.
Manage Groups and Members
The workflow page this recipe builds on.
Sync Users to External System
The companion recipe — user sync as the foundation.