Skip to main content

Teams

A team is a persistent, named group of players, scoped to your app, with membership, roles, and permissions the app controls. A game creates, joins, and manages teams live from UCrowdyTeams; Crowdy Studio's Teams pane edits the same rows ahead of time.

When you touch this​

Any time players need a group that outlives one session, a guild, a faction, a village, with roles you can grant and revoke. In the example world a village is a team: joining one is how a player shows they belong to a place.

Runtime and Studio, one dataset​

Creating, updating, and deleting teams is not a Studio-only workflow. The same create, update, delete, role, and policy operations run from game code through UCrowdyTeams, subject to your app's team creation policy, and Crowdy Studio's Teams pane reads and writes the identical server rows. There is one store, not two.

Membership, roles, and permissions are server-authoritative truth, pulled through UCrowdyTeams rather than trusted from the client, the same split every server-owned system in the SDK follows: see The Two Planes for the general story. There is no container here, just the Team API UCrowdyTeams wraps one call at a time. Joining a team does not put a player in a match; see Sessions and presence.

The server owns team, role, and permission storage and every authorization check; a disallowed call comes back through the error delegate rather than being refused on the client. The exact GraphQL shape behind each call is on Teams and in the operations reference; a team can also be granted permissions over a grid region, covered on Grids and permissions.

Every call below is asynchronous and answers through exactly one of the two delegates it is given, even when the request never reaches the server.

Reading the cache first​

GetCachedMyTeams and its neighbors read a local snapshot of the player's own memberships. The SDK keeps it current for you:

  • Signing in (log in, register, magic link, social, or a restored session) clears it and fills it again with a GetMyTeams.
  • Signing out clears it: HasCachedTeams() is false, and OnMyTeamsCacheChanged fires with an empty list if the cache held anything.
  • A successful CreateTeam, JoinTeam, LeaveTeam, DeleteTeam, UpdateTeam, RemoveTeamMember, SetTeamMemberRoles, UpdateTeamRole or DeleteTeamRole refreshes it. RequestToJoin does not, since the membership is still pending.
  • An answer to a GetMyTeams sent before a sign-out never fills the cache, and an older answer never replaces a newer one.

Read it for the value to show now; treat a query's result and OnMyTeamsCacheChanged as the update that follows. In C++, ClearMyTeamsCache() empties it by hand and RefreshMyTeams() asks for the player's teams again, with the answer arriving through OnMyTeamsCacheChanged; neither is exposed to Blueprint.

CallReturns
HasCachedTeams()Whether the cache has been populated yet.
GetCachedMyTeams()The player's memberships (TArray<FCrowdyTeamMembership>) as of the last refresh.
IsPlayerInTeam(TeamId)Whether the player belongs to that team, from the cache.
GetMyTeamById(TeamId, OutMembership)The cached membership for one team, if any.
IsInAnyTeam()Whether the cache holds at least one membership.
GetPrimaryMembership(OutMembership)The first cached membership.
HasPermissionInTeam(TeamId, Permission)Whether the cached membership grants an ECrowdyTeamPermission.

OnMyTeamsCacheChanged (FOnMyTeamsCacheChanged, Memberships) is a multicast, Blueprint-assignable delegate that fires whenever the cache changes: after a GetMyTeams answers, after a mutation above refreshes it, and when a sign-out empties it. Bind it once and re-read the cache; do not poll.

Queries​

Each query takes an FOnTeamError alongside its success delegate; bind it to a UFUNCTION with the signature (FCrowdyTeamError Error, FString Message), where Message repeats Error.Message.

CallSuccess delegateNotes
GetMyTeams(OnSuccess, OnError)FOnMyTeamsSuccessRepopulates the cache and fires OnMyTeamsCacheChanged.
GetTeam(TeamId, OnSuccess, OnError)FOnTeamSuccessOne team by id.
GetTeams(OnSuccess, OnError)FOnTeamsSuccessEvery team in the app, not just the player's.
GetTeamMembers(TeamId, OnSuccess, OnError)FOnTeamMembersSuccess
GetTeamRoles(TeamId, OnSuccess, OnError)FOnTeamRolesSuccess
GetTeamPolicy(OnSuccess, OnError)FOnTeamPolicySuccessApp-wide, not per-team.
GetPendingJoinRequests(TeamId, OnSuccess, OnError)FOnTeamMembersSuccessSee the warning below.
GetPendingJoinRequests re-fetches the full member list and filters on the client.

It runs the same query as GetTeamMembers and then keeps only the members whose status is pending. That is fine at team scale, a handful to a few dozen members, but do not call it in a hot loop expecting a cheaper, dedicated request.

Creating and managing a team​

Every mutation reports success through its own delegate and failure through the shared FOnTeamError.

Team lifecycle: CreateTeam(Name, Description, MembershipPolicy, OnSuccess, OnError) returns FOnTeamSuccess with the new FCrowdyTeam; UpdateTeam(TeamId, Name, Description, OnSuccess, OnError) also returns FOnTeamSuccess; DeleteTeam(TeamId, OnSuccess, OnError) returns FOnTeamVoidSuccess.

Membership: JoinTeam(TeamId, OnSuccess, OnError) honors the team's ECrowdyTeamMembershipPolicy, seating the player immediately for an Open team; RequestToJoinTeam(TeamId, OnSuccess, OnError) is for a Request policy team and returns a pending member. Both return FOnTeamMemberSuccess. LeaveTeam(TeamId, OnSuccess, OnError) returns FOnTeamVoidSuccess. AddTeamMember(TeamId, UserId, OnSuccess, OnError) is the admin add that bypasses the membership policy; RemoveTeamMember(TeamId, UserId, OnSuccess, OnError) takes them off. AddTeamMember answers with FOnTeamMemberSuccess, RemoveTeamMember with FOnTeamVoidSuccess.

Roles and permissions: CreateTeamRole(TeamId, RoleName, Permissions, Rank, OnSuccess, OnError) and UpdateTeamRole(TeamRoleId, RoleName, Permissions, OnSuccess, OnError) return FOnTeamRoleSuccess; DeleteTeamRole(TeamRoleId, OnSuccess, OnError) returns FOnTeamVoidSuccess. SetTeamMemberRoles(TeamId, UserId, RoleIds, OnSuccess, OnError) returns FOnTeamMemberSuccess. Permissions is an FCrowdyTeamPermissions your code builds before the call.

SetTeamMemberRoles replaces the member's roles, it does not add to them.

Pass every role the member should end up holding, including ones they already have, or the call drops the roles you left out.

App-wide policy: SetTeamPolicy(CreationPolicy, DefaultMembershipPolicy, OnSuccess, OnError) returns FOnTeamPolicySuccess and is an app-admin operation.

Ban a member​

A manager removes a member for good with BanTeamMember(TeamId, UserId, Reason, OnSuccess, OnError). It takes the user off the team and refuses every later join, request to join and add of that user until a manager lifts the ban with UnbanTeamMember(TeamId, UserId, OnSuccess, OnError). The caller needs the manage-members permission, and the team's owner and the caller themselves cannot be banned. A user who is not on the team yet can be banned too, and leaving or being removed does not lift a ban.

Reason is optional and at most 500 characters. A longer one fails at once with a PolicyViolation error, and nothing is sent. Ban answers the member record (FOnTeamMemberSuccess), whose Status is banned; unban answers bWasBanned (FOnTeamUnbanSuccess), which is false when the user had no ban.

A banned user's JoinTeam, RequestToJoinTeam, or a manager's AddTeamMember for them, fails with the error code Banned in ECrowdyTeamErrorCode. It comes from the server's own error code, not from the message text, so it is safe to branch on. A ban lasts until a manager lifts it, so do not retry: tell the player instead.

The village's elder bans a member from the village team. VillageTeamId is the id the game instance kept when it founded the village.

A Ban button on the member list calls BanFromVillage with the member picked. HandleVillageBanned receives the member record, and HandleVillageBanFailed the error.

// LanternGameInstance.h
#include "Subsystem/CrowdyTeams.h"

public:
void BanFromVillage(int64 UserId, const FString& Reason);

UFUNCTION()
void HandleVillageBanned(FCrowdyTeamMember Member);

UFUNCTION()
void HandleVillageBanFailed(FCrowdyTeamError Error, FString Message);

// LanternGameInstance.cpp
void ULanternGameInstance::BanFromVillage(int64 UserId, const FString& Reason)
{
UCrowdyTeams* Teams = GetSubsystem<UCrowdyTeams>();
FOnTeamMemberSuccess Banned;
Banned.BindDynamic(this, &ULanternGameInstance::HandleVillageBanned);
FOnTeamError Failed;
Failed.BindDynamic(this, &ULanternGameInstance::HandleVillageBanFailed);
Teams->BanTeamMember(VillageTeamId, UserId, Reason, Banned, Failed);
}

void ULanternGameInstance::HandleVillageBanned(FCrowdyTeamMember Member)
{
UE_LOG(LogTemp, Log, TEXT("User %lld is banned from the village"), Member.UserId);
}

void ULanternGameInstance::HandleVillageBanFailed(FCrowdyTeamError Error, FString Message)
{
UE_LOG(LogTemp, Warning, TEXT("The ban failed: %s"), *Message);
}

Data types​

StructHolds
FCrowdyTeamTeamId, AppId, Name, Description, OwnerUserId, MembershipPolicy, Status, CreatedAt.
FCrowdyTeamMemberTeamMemberId, TeamId, UserId, Status (active, pending, or banned in the answer to a ban), Roles sorted by rank, CreatedAt.
FCrowdyTeamMembershipTeam, Roles, the effective Permissions (a server-computed union across roles), JoinedAt, and HasPermission(ECrowdyTeamPermission).
FCrowdyTeamPermissionsFlags bManageTeam, bManageMembers, bManageRoles, bInviteMembers, all BlueprintReadWrite, and Has(). Build one to pass into CreateTeamRole or UpdateTeamRole.
FCrowdyTeamPolicyAppId, CreationPolicy, DefaultMembershipPolicy, MaxMembers (0 is uncapped), MaxTeamsPerUser (0 is uncapped).
FCrowdyTeamRoleTeamRoleId, TeamId, RoleName, Rank, bIsSystem (every team has an undeletable system leader role), Permissions, CreatedAt.
FCrowdyTeamErrorCode (ECrowdyTeamErrorCode) and Message.
EnumValues
ECrowdyTeamCreationPolicyAdmin, Member, Anyone
ECrowdyTeamMembershipPolicyOpen, Request, Invite, Admin
ECrowdyTeamPermissionManageTeam, ManageMembers, ManageRoles, InviteMembers
ECrowdyTeamErrorCodeUnknown, NotFound, Forbidden, PolicyViolation, AlreadyMember, NotMember, NetworkError, ServerError, Banned
Code is a coarse bucket, not a precise diagnosis, except Banned.

FCrowdyTeamError is built by pattern-matching the server's message text, so every Code but one is a guess. Banned is the exception: it comes from the server's own error code. Read Message for the detail and branch on Code only for the broad cases.

Blueprint nodes​

The subsystem's per-call delegates need a Create Event node per call. For a graph that just drops in a node, use the async-action latent nodes instead: one static factory and Activate() per node, with multicast OnSuccess/OnError, wrapping the same server calls.

NodeClassReturns
Get My TeamsUCrowdyTeams_GetMyTeamsFCrowdyMyTeamsResult
Get TeamUCrowdyTeams_GetTeamFCrowdyTeam
Get All TeamsUCrowdyTeams_GetTeamsFCrowdyTeamsResult
Get Team MembersUCrowdyTeams_GetTeamMembersFCrowdyTeamMembersResult
Get Team RolesUCrowdyTeams_GetTeamRolesFCrowdyTeamRolesResult
Get Team PolicyUCrowdyTeams_GetTeamPolicyFCrowdyTeamPolicy
Get Pending Join RequestsUCrowdyTeams_GetPendingJoinRequestsFCrowdyTeamMembersResult
Create TeamUCrowdyTeams_CreateTeamFCrowdyTeam
Update TeamUCrowdyTeams_UpdateTeamFCrowdyTeam
Delete TeamUCrowdyTeams_DeleteTeamvoid
Join TeamUCrowdyTeams_JoinTeamFCrowdyTeamMember
Request to Join TeamUCrowdyTeams_RequestToJoinTeamFCrowdyTeamMember
Leave TeamUCrowdyTeams_LeaveTeamvoid
Add Team MemberUCrowdyTeams_AddTeamMemberFCrowdyTeamMember
Remove Team MemberUCrowdyTeams_RemoveTeamMembervoid
Ban Team MemberUCrowdyTeams_BanTeamMemberFCrowdyTeamMember
Unban Team MemberUCrowdyTeams_UnbanTeamMemberWas Banned (bool)
Set Team Member RolesUCrowdyTeams_SetTeamMemberRolesFCrowdyTeamMember
Create Team RoleUCrowdyTeams_CreateTeamRoleFCrowdyTeamRole
Update Team RoleUCrowdyTeams_UpdateTeamRoleFCrowdyTeamRole
Delete Team RoleUCrowdyTeams_DeleteTeamRolevoid
Set Team PolicyUCrowdyTeams_SetTeamPolicyFCrowdyTeamPolicy

FCrowdyTeamsResult, FCrowdyMyTeamsResult, FCrowdyTeamMembersResult, and FCrowdyTeamRolesResult each wrap one multi-item result (Teams, Memberships, Members, Roles); a single-item call like GetTeam or CreateTeam returns its struct directly.

On Success and On Error of a node share one set of data pins: the result, then Error and Message. On On Success, Error and Message are empty. On On Error the result is empty, Error is an FCrowdyTeamError (break it to read its Code) and Message is the text to show; a node that returns nothing has only Error and Message.

The pins are a multicast delegate named after the result: FTeamAsyncResult (Get Team, Create Team, Update Team), FTeamsAsyncResult (Get All Teams), FMyTeamsAsyncResult (Get My Teams), FMemberAsyncResult (Join Team, Request to Join Team, Add Team Member, Ban Team Member, Set Team Member Roles), FMembersAsyncResult (Get Team Members, Get Pending Join Requests), FRoleAsyncResult (Create Team Role, Update Team Role), FRolesAsyncResult (Get Team Roles), FPolicyAsyncResult (Get Team Policy, Set Team Policy), FTeamUnbanAsyncResult (Unban Team Member), and FTeamsAsyncOnError (Delete Team, Leave Team, Remove Team Member, Delete Team Role). These are separate from the per-call FOnTeam*Success delegates above; the async-action nodes never use those.

Bind a per-call delegate to a UFUNCTION on a UObject that outlives the request.

The response arrives asynchronously; if the object is gone by then, the callback has nowhere to land.

The array delegates pass const TArray<T>&.

FOnTeamsSuccess, FOnTeamMembersSuccess, FOnTeamRolesSuccess, FOnMyTeamsSuccess, and the cache event FOnMyTeamsCacheChanged pass their array as const TArray<T>&, which is what the C++ handlers on this page take. A handler must match or it fails to bind, and a project coming from 2.14.0 or earlier, where these passed by value, changes its handlers once; see What's Changed. In Blueprint prefer the async-action nodes above, whose result pins are structs; a Blueprint pin has no by-value distinction.

Example: founding and joining a village​

A village is a team. The player asks to join "the Village," and a successful join lights their Torch, the light component the example cast already carries, to show membership at a glance.

A "Found the Village" button calls FoundVillage(), which calls CreateTeam("The Village", "Everyone who shares one fire.", Open, HandleVillageFounded, FOnTeamError()); HandleVillageFounded(FCrowdyTeam) stores the new id in a private VillageTeamId, read back through the BlueprintPure GetVillageTeamId():

// LanternGameInstance.h
#include "Subsystem/CrowdyTeams.h"

public:
// A Found the Village button calls this; the id the handler keeps is the one every player joins.
UFUNCTION(BlueprintCallable, Category = "Lantern")
void FoundVillage();

UFUNCTION(BlueprintPure, Category = "Lantern")
int64 GetVillageTeamId() const { return VillageTeamId; }

private:
int64 VillageTeamId = 0;

UFUNCTION()
void HandleVillageFounded(FCrowdyTeam Team);

// LanternGameInstance.cpp
void ULanternGameInstance::FoundVillage()
{
UCrowdyTeams* Teams = GetSubsystem<UCrowdyTeams>();
FOnTeamSuccess Founded;
Founded.BindDynamic(this, &ULanternGameInstance::HandleVillageFounded);
Teams->CreateTeam(TEXT("The Village"), TEXT("Everyone who shares one fire."),
ECrowdyTeamMembershipPolicy::Open, Founded, FOnTeamError());
}

void ULanternGameInstance::HandleVillageFounded(FCrowdyTeam Team)
{
VillageTeamId = Team.TeamId;
}

Overlapping the village post calls JoinVillage(), which calls JoinTeam(GetVillageTeamId(), ...); on success HandleJoinedVillage(FCrowdyTeamMember) sets Torch->SetVisibility(true):

// LanternPlayer.h
#include "Subsystem/CrowdyTeams.h"

virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;

void JoinVillage();

UFUNCTION()
void HandleJoinedVillage(FCrowdyTeamMember Member);

// LanternPlayer.cpp
#include "LanternGameInstance.h"
#include "LanternPost.h"

void ALanternPlayer::NotifyActorBeginOverlap(AActor* OtherActor)
{
Super::NotifyActorBeginOverlap(OtherActor);
if (Cast<ALanternPost>(OtherActor))
{
JoinVillage();
}
}

void ALanternPlayer::JoinVillage()
{
const ULanternGameInstance* Game = GetGameInstance<ULanternGameInstance>();
UCrowdyTeams* Teams = Game ? Game->GetSubsystem<UCrowdyTeams>() : nullptr;
if (!Teams || Game->GetVillageTeamId() == 0)
{
return;
}
// Open membership seats the player at once; a Request team would answer with a pending member instead.
FOnTeamMemberSuccess Joined;
Joined.BindDynamic(this, &ALanternPlayer::HandleJoinedVillage);
Teams->JoinTeam(Game->GetVillageTeamId(), Joined, FOnTeamError());
}

void ALanternPlayer::HandleJoinedVillage(FCrowdyTeamMember Member)
{
Torch->SetVisibility(true);
}

ShowVillageMembership(), called from BeginPlay, guards on HasCachedTeams() and sets Torch->SetVisibility(Teams->IsPlayerInTeam(GetVillageTeamId())) from the cache alone, before any round trip completes:

// LanternPlayer.h
void ShowVillageMembership();

// LanternPlayer.cpp
#include "LanternGameInstance.h"
#include "Subsystem/CrowdyTeams.h"

void ALanternPlayer::ShowVillageMembership()
{
const ULanternGameInstance* Game = GetGameInstance<ULanternGameInstance>();
UCrowdyTeams* Teams = Game ? Game->GetSubsystem<UCrowdyTeams>() : nullptr;
// The cache holds this player's own memberships only, and nothing until the first Get My Teams has answered.
if (!Teams || !Teams->HasCachedTeams())
{
return;
}
Torch->SetVisibility(Teams->IsPlayerInTeam(Game->GetVillageTeamId()));
}

WatchVillage(), bound at BeginPlay, calls OnMyTeamsCacheChanged.AddDynamic and GetMyTeams(); HandleVillageCacheChanged(const TArray<FCrowdyTeamMembership>&) re-runs ShowVillageMembership() whenever the cache changes, from anywhere:

// LanternPlayer.h
#include "Subsystem/CrowdyTeams.h"

void WatchVillage();

UFUNCTION()
void HandleVillageCacheChanged(const TArray<FCrowdyTeamMembership>& Memberships);

// LanternPlayer.cpp
void ALanternPlayer::WatchVillage()
{
UCrowdyTeams* Teams = GetGameInstance()->GetSubsystem<UCrowdyTeams>();
Teams->OnMyTeamsCacheChanged.AddDynamic(this, &ALanternPlayer::HandleVillageCacheChanged);
// Only Get My Teams fills the cache; a join or a leave does not, so pull again after one.
Teams->GetMyTeams(FOnMyTeamsSuccess(), FOnTeamError());
}

void ALanternPlayer::HandleVillageCacheChanged(const TArray<FCrowdyTeamMembership>& Memberships)
{
ShowVillageMembership();
}

Gotchas​

  • The cache holds only the player's own memberships. GetTeams lists every team in the app; the cache never does.
  • A successful join, leave, create, delete or role change refreshes the cache, but a pending RequestToJoin does not, since the player is not a member yet. Between a change and its refresh, IsPlayerInTeam still answers from before it.
  • GetPendingJoinRequests filters the full member list on the client; it is not a dedicated server call.
  • SetTeamMemberRoles replaces the member's role set. Pass the full set every time.
  • FCrowdyTeamError::Code is pattern-matched from the server's message text, except Banned, which is the server's own code; treat the rest as a coarse bucket and Message as the detail.
  • A per-call delegate needs a UFUNCTION on a UObject that outlives the request, including when the request never reaches the server.
  • The array delegates take const TArray<T>& handlers; a mismatch fails to bind. The async-action nodes in Blueprint have no such split.