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
| Scenario | This recipe fits |
|---|---|
| ”Volunteers who served in 2024” Group for reporting | ✓ |
| “Marketing department” Group mirrored from HR system | ✓ With external roster as the query source |
| ”Recent first-time volunteers” Group for onboarding workflows | ✓ |
| “Holders of the Food Handler certification” Group for eligibility | ✓ With Certificate data |
| Truly dynamic “users matching X right now” Group | ✗ Groups are snapshots — see the dynamic-query trap |
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:| Pattern | Refresh model |
|---|---|
| Snapshot Group | Build once, accept that it ages |
| Periodically refreshed Group | Re-run the query daily/weekly/monthly, update members to match |
| Event-driven refresh | Re-evaluate membership when triggering events occur |
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:| Component | Purpose |
|---|---|
| Query specification | The criteria definition + target Group ID + refresh cadence |
| Refresh scheduler | Triggers re-evaluation on a schedule |
| Query resolver | Translates criteria into a current set of User IDs |
| Group manager | Creates the Group if needed; replaces members on each refresh |
| Diff calculator | Compares previous and current member sets; produces an audit trail |
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
| Criteria | Suggested cadence |
|---|---|
| ”Created after X” (static date) | Daily — new users added; never removed |
| ”Email domain” | Daily — emails change rarely |
| ”Served during 2024” (historical window) | Weekly — window is closed |
| ”Served in the last 90 days” (rolling window) | Daily — window moves forward each day |
| ”Has Certificate X” | Daily — certificates earned periodically |
| ”External roster” | Hourly to daily depending on source frequency |
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.