Nested Google Groups and Slack: How to Flatten Membership Correctly
SyncMyGroup team · October 9, 2026
Most Google Workspace tenants that have been around for a few years have nested groups. engineering@ contains platform@, payments@ and mobile@. everyone@ contains every department group. oncall-all@ contains three team on-call lists.
That's a sensible way to structure email. It becomes a problem the moment you try to mirror those groups somewhere that doesn't nest, and Slack user groups don't. A Slack user group is a flat list of people.
This post covers how nesting actually behaves in Google, what Slack expects on the other end, and how to flatten one into the other without the classic mistakes: syncing three "members" instead of sixty, losing people who are only indirect members, or quietly adding your entire domain to a handle.
How nesting works in Google Groups
A Google Group's members can be individual users or other groups. When you add platform@ to engineering@, the people in platform@ are now indirect members of engineering@.
A few behaviours from Google's admin help (2026) matter for sync:
- Child members aren't listed in the parent. People in a child group don't appear in the parent's member list, can't leave the parent directly, and can't change their membership settings there. If you look at
engineering@in the admin console, you seeplatform@, not the people in it. - Nesting can be switched off. Whether a group may contain other groups is controlled by its member restrictions. Allowing groups as a member type is what enables nesting.
- Membership limits count indirect memberships. Google raised its limits in 2023: a user or group can be a direct member of about 3,000 groups, and about 7,500 counting indirect memberships.
- Email delivery isn't the same as membership. Mail sent to a parent is filtered out for external members of nested child groups, and internal child members only receive parent mail if the child group's posting settings allow it. "Gets the email" and "is a member" aren't always the same set of people.
That last point is worth keeping in mind. If someone tells you "but they get the engineering@ emails," that's not proof they're a member in the sense a sync tool will see.
What the API gives you
The Admin SDK Directory API's members.list method returns the members of a group. Each member has a type:
type | Meaning |
|---|---|
USER | An individual user |
GROUP | Another group (this is the nesting) |
CUSTOMER | All users in your domain. No email is returned, only the customer ID |
EXTERNAL | Documented as "not currently used" |
By default members.list returns direct members only. To get indirect members, set includeDerivedMembership=true. Results page at up to 200 per request (maxResults), with nextPageToken for the rest.
So there are two ways to flatten:
- Call
members.listwithincludeDerivedMembership=trueand let Google expand the tree. - Call it without, and recurse into every
GROUPmember yourself.
Option 1 is less code. Option 2 gives you control over which branches to follow, which matters once you want to exclude certain child groups.
What Slack expects
Slack user groups are flat. The method that sets membership, usergroups.users.update, takes a comma-separated list of user IDs that represents the entire list of users for the group, and replaces whatever was there. Its documentation adds two details that matter for sync:
- You can't use it to empty a group. Passing no users returns
no_users_provided; to take everyone out, you disable the group withusergroups.disable. - It's a Tier 2 method (20+ requests per minute), so a sync across many groups needs to pace itself.
Slack's help center also notes that guests, and people you work with through Slack Connect, can't be added to your organisation's user groups. If your Google Group contains contractors who are guests in Slack, they will never land in the user group, no matter how well you flatten.
A minimal flattening script
Here's the shape of a correct flatten-and-write, in Python, using recursion so you can see where each decision happens:
def flatten(directory, group_key, seen=None, skip_groups=frozenset()):
"""Return the set of user emails that are direct or indirect members."""
seen = seen if seen is not None else set()
if group_key in seen: # guard against loops
return set()
seen.add(group_key)
emails, page = set(), None
while True:
resp = directory.members().list(
groupKey=group_key, maxResults=200, pageToken=page
).execute()
for m in resp.get("members", []):
if m["type"] == "USER" and not is_active(m["email"]):
continue # your policy for suspended accounts
if m["type"] == "USER":
emails.add(m["email"].lower())
elif m["type"] == "GROUP" and m["email"] not in skip_groups:
emails |= flatten(directory, m["email"], seen, skip_groups)
elif m["type"] == "CUSTOMER":
raise ValueError(f"{group_key} contains the whole domain")
page = resp.get("nextPageToken")
if not page:
return emails
Then resolve each email to a Slack user ID with users.lookupByEmail, drop anyone who isn't found or is a guest, and call usergroups.users.update with the full list.
Three choices in that function deserve a comment:
- Loop guard. If
a@containsb@andb@containsa@, a naive recursion never ends. Tracking visited groups costs one line. - The
CUSTOMERmember. If someone has added "all users in the domain" to a group, flattening it produces your whole company. Raising an error is deliberate. You almost never want that to reach a Slack handle silently. - Suspended users.
is_activeis a placeholder for your own check (for example, a lookup against the Directory API'susersresource). Decide explicitly what to do with suspended accounts. Skipping them means the sync removes them from Slack, which is usually what offboarding wants.
Design decisions before you sync
The code is the easy part. These decisions are what make the result useful.
Flatten the parent, mirror the children, or both?
Given engineering@ containing platform@, payments@ and mobile@, you have three options:
| Approach | Slack result | Good for |
|---|---|---|
| Flatten the parent only | @engineering with everyone | Org-wide mentions |
| Mirror each child | @platform, @payments, @mobile | Team-level mentions and channels |
| Both | All four handles | Most engineering orgs |
"Both" is usually right. Each mapping stays simple — one Google Group to one Slack user group — and the nesting lives only on the Google side, where it already works.
Exclude branches you don't want
Parent groups often contain children that don't belong in the Slack handle: an engineering-alumni@ list, a vendor distribution list, a shared mailbox. Decide which child groups to skip before the first sync, not after someone notices a vendor in @engineering.
Owners and managers are members too
Google Group members have a role: OWNER, MANAGER or MEMBER. All three are members. If the group's owner is an IT admin who isn't on the team, they'll be flattened into the Slack handle unless you exclude them.
People who aren't in the source but should stay
Every real group has a few: the bot that posts deploy notices, an engineering manager who sits in a different Google Group, an executive who wants visibility. Decide whether to add them to the Google Group or keep them as explicit exceptions on the Slack side. Don't let the first sync remove them and find out from a complaint.
Common mistakes
Syncing direct members only. The most common one. Without derived membership or recursion, engineering@ resolves to three group addresses, none of which are Slack users, and the handle ends up with only the handful of people added directly.
Treating group addresses as people. A naive script that sends every member email to users.lookupByEmail will get "user not found" for each child group and may log it as a mismatch rather than a nested group to expand.
Flattening into a destination that already nests. Some destinations, like GitHub teams with child teams, have their own hierarchy. Flatten into flat destinations; think twice before flattening into hierarchical ones.
Ignoring email mismatches. Someone's Google primary is priya@company.com and their Slack email is p.sharma@company.com. Flattening is correct; matching fails. Review the list of unmatched people after every first sync.
Doing it with SyncMyGroup
If you'd rather not maintain the script, SyncMyGroup supports nested Google Groups when syncing to Slack user groups and channels, and to the other destinations it covers. The setup for a nested parent is the same as for any group:
- Connect Google Workspace and Slack.
- Pick the parent Google Group as the source and a Slack user group (or channel) as the destination.
- Use filters to exclude child groups or individual users you don't want flattened in.
- Add permanent users for the bots and exceptions that should stay regardless of the source.
- Read the preview. On a nested parent, it's the fastest way to check the flattening matches what you expect. If the "add" list is suspiciously short, the parent probably has members you didn't know about; if it's suspiciously long, look for a
CUSTOMERmember or an alumni list. - Subscribe a channel to change notifications with
@SyncMyGroups subscribe <team>so you can see each add and remove.
For the basics of the Google → Slack mapping, see how to sync Google Groups to Slack user groups. For channel membership specifically, see syncing Slack channel membership with Google Groups. If you're new to how Slack user groups behave, start with the Slack user groups guide.
A quick checklist
Before you flatten a nested group into Slack:
- Listed every child group, at every level.
- Checked for a "all users in domain" member.
- Decided which child groups to exclude.
- Decided whether owners and managers belong in the handle.
- Identified the bots and exceptions to keep.
- Checked how many members are Slack guests (they can't join user groups).
- Reviewed the preview and the list of people who couldn't be matched.
FAQ
Can a Slack user group contain another user group?
No. Slack's usergroups.users.update method takes a list of user IDs and replaces the group's entire membership with it. There's no way to add a group as a member, so nested structure has to be flattened before it reaches Slack.
Does Google's API flatten nested groups for me?
Yes, if you ask. members.list returns direct members by default. Set includeDerivedMembership=true to include indirect members. If you need to skip some child groups, recurse yourself instead.
Why does my nested group sync miss people who receive the group's email?
Email delivery and membership aren't the same thing in Google Groups. Posting settings and external-member rules affect who receives mail sent to a parent group. A sync reads membership, so check the actual member tree rather than the mail recipients.
What happens to Slack guests who are in the Google Group?
They're skipped. Slack doesn't allow guests or Slack Connect users in your organisation's user groups. If they need access, add them to the relevant channels instead, or make them full members.
Point SyncMyGroup at your most nested Google Group and look at the preview. It's free up to 5 users, and the Google Group → Slack workflow takes a few minutes to set up.