Skip to main content
A common Volunteer integration pattern: define a group of users by some criteria rather than by manual selection — “all volunteers who served in 2024,” “everyone from the Marketing department,” “users who completed the food handler certification,” “first-time volunteers from the past 90 days.” VOMO Groups are perfect destinations for these query-derived collections, but Groups are snapshots, not self-updating dynamic queries. This recipe walks through the pattern: query criteria → user IDs → Group creation/update → ongoing maintenance. The recipe combines listing users with filters, Group management, and reconciliation into a complete query-to-Group workflow.

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
The criteria is structured — different criteria types call different resolver paths:
JavaScript
Each criterion type maps to a resolver function.

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
Cost: Substantial. For an active customer, this is many API requests per resolution. Cache the intermediate results (Project detail, Project Date detail) and pace the resolver.

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
This is N+1. For large accounts, consider caching the certificate-by-user mapping in your integration’s state DB.

Resolver: “external roster”

JavaScript
The find-or-create-then-add pattern: external system is authoritative for membership; VOMO is the destination.

Resolver: combined criteria

JavaScript
AND and OR cover most needs. NOT is typically expressed as “in set A but not in set B” — a derived combination.

Step 3: create or update the Group

JavaScript
The audit log captures what changed at each refresh — useful for customer-facing transparency and debugging.

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
The history shows “47 yesterday; 52 today; +6 new; -1 removed” — a clear activity signal.

Common queries with full specifications

Volunteers who served in the last 90 days

JavaScript
The dynamic 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
For large user lists, this is expensive. Alternative: don’t pre-filter; let the PUT fail; capture errors and retry without the missing IDs.

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
Alert on >50% swing in member count between consecutive refreshes.

Group hierarchy considerations

If the target Group has a parent_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 like served_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
This is the foundation for any “Group from query” workflow — historical-volunteer Groups, eligibility Groups, department-mirror Groups.

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.
Last modified on May 22, 2026