Client
The client-side data API: bundle.Client. Read the local player’s data
through the same accessor tree as the server (Data.Coins.Get(),
Data.Coins.Observe(fn)); those methods are documented on the Value
class. Writes are local-only (optimistic UI). Server ops always win.
To change data authoritatively, call a server command via Client.Request.
In edit mode (when RunService:IsRunning() is false, e.g. a storybook)
the client skips the transport and initializes instantly to template
defaults, so components render without a running server.
Find a member
Section titled “Find a member”Choose a group, then follow a member link for its signature and behavior.
Lifecycle
IsReady · WaitForData · Batch · Stop
Commands
Leaderboards
GetLeaderboard · GetMyRank · OnLeaderboard
Service
GetServiceStatus · OnServiceStatus
Shared Data
Ownership
OnOwnershipChanged · Owns · OwnsAsync · ObserveOwned
Monetization
GetProductInfo · GetPrice · GetProductInfoAsync · GetPriceAsync · ObserveProductInfo · PrefetchProductInfo · RefreshProductInfo · GetProductState · PromptPurchase · ObserveProductState · GetGiftCredits · GetPurchases
Reads
GetSaveInfo · Raw · ServerStore
Edit Mode
Lifecycle
Section titled “Lifecycle”.IsReady
Section titled “.IsReady”Client.IsReady() → booleanWhether the first Init snapshot has arrived (non-yielding). Before this is
true, accessor reads return template defaults. Always true in edit mode.
.WaitForData
Section titled “.WaitForData”Client.WaitForData(timeout: number?) → booleanYields until the first Init snapshot arrives, then returns true. Returns
false if timeout seconds (default 30) pass first. It never hangs. Use
it to gate startup logic; for reactive UI prefer Observe/Changed.
if Data.WaitForData() then showMainMenu(Data.Coins.Get())end.Batch
Section titled “.Batch”Client.Batch(fn: () -> ())Runs fn as a batch: every local write inside coalesces into one Changed pass
per container, the same way Server.Batch does on the server. Client writes are
optimistic and never leave the client, so this changes nothing on the wire; it
only stops a listener firing once per write when a UI updates several fields at
once.
Frames the server sends are already applied as one batch each, so a
Server.Batch arrives as one fire without any call here.
Data.Batch(function() Data.Equipped.Set("Sword") Data.Inventory.Sword.Qty.Decrement(1)end)-- Data.Inventory.Changed fires ONCEClient.Stop()Releases what this client bundle holds on the process: the transport listener,
the Hello retry loop, the subscription watch behind Client.GetProductInfo, and
the mirror tree’s listeners. The counterpart of Server.Stop.
Product-info retries and observers stop; pending info/price reads return nil.
A game never needs it. A storybook that remounts a bundle per story, or a test suite that builds many, otherwise stacks a listener per build. Idempotent.
Commands
Section titled “Commands”.Request
Section titled “.Request”Client.Request(name: string, ...: any) → ...anyCalls a server command registered with Server.Command and returns whatever
its handler returned. It yields until the reply arrives, and resolves
false, "timeout" if none comes within RequestTimeout. This is the one way
a client changes data authoritatively.
When Scribe refuses rather than your handler, the result is false, one
Scribe.RequestReason, and Scribe.RequestFailed in a third slot. Test that
third value, because a handler may return false, "timeout" of its own. A
retry after a timeout runs the handler a second time, so reach for
Client.RequestOnce when that is not safe. See
Commands & Requests.
.RequestOnce
Section titled “.RequestOnce”Client.RequestOnce(name: string, key: string, ...: any) → ...anyLike Client.Request, but tagged with an idempotency key: the server runs
the handler at most once per key and replays its original reply, byte for
byte, to every repeat. Use it for anything a retry must not do twice, and
generate the key once per intent rather than once per attempt.
The command must be registered Idempotent = true, and a key sent to one that
is not, or a keyless Client.Request to one that is, is refused with
"bad-idempotency-key". A key must be non-empty, valid UTF-8, and at most 64
bytes, and this call raises on your own thread rather than send one that is
not. See
Running a command at most once.
Leaderboards
Section titled “Leaderboards”.GetLeaderboard
Section titled “.GetLeaderboard”Client.GetLeaderboard(name: string, limit: number?) → { LeaderboardEntry }Types: LeaderboardEntry
The cached top-N of a replicated board (Replicate = true), streamed
to clients at handshake and on change. Empty for server-only boards.
.GetMyRank
Section titled “.GetMyRank”Client.GetMyRank(name: string) → number?The local player’s rank on a replicated board, or nil if unranked.
.OnLeaderboard
Section titled “.OnLeaderboard”Client.OnLeaderboard: SignalFires (boardName, entries) whenever a replicated board updates.
The value is a Signal, where Connect, Once and Wait are documented.
Service
Section titled “Service”.GetServiceStatus
Section titled “.GetServiceStatus”Client.GetServiceStatus() → "Healthy" | "Degraded" | "Outage"The replicated data-service health. Show players a “progress may save
late” notice during a "Degraded"/"Outage".
.OnServiceStatus
Section titled “.OnServiceStatus”Client.OnServiceStatus: SignalFires with the new status whenever service health changes.
The value is a Signal, where Connect, Once and Wait are documented.
Shared Data
Section titled “Shared Data”.GetShared
Section titled “.GetShared”Client.GetShared(playerOrUserId: Player | number) → { [string]: any }?Reads another player’s Scribe.Shared roots (public info replicated to
everyone), or nil if not yet received.
Never returns the local player’s own roots: the server broadcasts a
player’s Shared data to everyone except that player, so nil there is
permanent rather than pending. Read your own through the ordinary accessor.
.OnSharedChanged
Section titled “.OnSharedChanged”Client.OnSharedChanged: SignalFires (userId, sharedData) when another player’s Shared data changes.
The value is a Signal, where Connect, Once and Wait are documented.
Ownership
Section titled “Ownership”.OnOwnershipChanged
Section titled “.OnOwnershipChanged”Client.OnOwnershipChanged: SignalFires (key, owned) whenever the local player’s ownership of a pass or perk
actually changes. Ownership already held at load is the baseline and does not
fire. Use Client.ObserveOwned instead when you care about one specific key
and want the current value immediately.
The value is a Signal, where Connect, Once and Wait are documented.
Client.Owns(key: string) → booleanWhether the local player owns a perk or pass, from the replicated mirror. Yields until data has loaded.
.OwnsAsync
Section titled “.OwnsAsync”Client.OwnsAsync(key: string, timeout: number?) → booleanLike Client.Owns, but yields until the server has finished syncing
ownership (the initial async gamepass refresh, signalled by a replicated
flag). Use it so a genuinely-owned pass is never briefly reported un-owned in
the window right after join. Perks/gifts are already synced at load. Falls
back to the current mirror value after timeout seconds (default 10).
.ObserveOwned
Section titled “.ObserveOwned”Client.ObserveOwned(key: string, callback: (owned: boolean) -> ()) → () -> ()Calls callback with the current ownership and again whenever it changes.
Returns a disconnect function. Ideal for reactively toggling “buy” buttons.
Monetization
Section titled “Monetization”.GetProductInfo
Section titled “.GetProductInfo”Client.GetProductInfo(name: string) → { [string]: any }?Marketplace info for a declared pass or product, as this player sees it.
PriceInRobux is what they are charged, after any Roblox Plus or regional
discount; UserBasePriceInRobux and PriceDiscountDetails are there for a
“was 100, now 80” button.
Returns nil until the first read lands, and starts that read. Poll it, or use
Client.ObserveProductInfo. Never waits for the profile to load.
Client-only by necessity: a live server has no player in context and reads the base catalog price, which is not what a discounted player pays.
.GetPrice
Section titled “.GetPrice”Client.GetPrice(name: string) → number?Shorthand for the PriceInRobux of Client.GetProductInfo: the Robux this
player pays. nil until the first read lands, and starts that read.
.GetProductInfoAsync
Section titled “.GetProductInfoAsync”Client.GetProductInfoAsync(name: string, timeout: number?) → { [string]: any }?Like Client.GetProductInfo, but yields until the read lands. Returns nil if it
times out (default 10s), Marketplace refused every attempt, or Client.Stop is called.
.GetPriceAsync
Section titled “.GetPriceAsync”Client.GetPriceAsync(name: string, timeout: number?) → number?Shorthand for the PriceInRobux of Client.GetProductInfoAsync: the Robux this
player pays. nil on timeout (default 10s), refusal, or Client.Stop.
.ObserveProductInfo
Section titled “.ObserveProductInfo”Client.ObserveProductInfo(name: string, callback: ({ [string]: any }?) -> ()) → () -> ()Calls callback with the info now (nil if it has not landed), again when it
does, and again if the price changes. Returns a disconnect function. The
reactive way to price a shop button.
.PrefetchProductInfo
Section titled “.PrefetchProductInfo”Client.PrefetchProductInfo(names: { string }?)Warms names, or every declared pass and product when omitted, as one batch.
Optional: every read path fetches on demand anyway. Worth calling before opening
a shop with many items, because one burst costs one request where the same reads
trickling in frame by frame cost one each.
.RefreshProductInfo
Section titled “.RefreshProductInfo”Client.RefreshProductInfo(name: string?)Drops the cached info for name, or for everything when omitted, and reads it
again. Scribe already does this the first time the local player’s
HasRobloxSubscription flips; use this for any later move.
.GetProductState
Section titled “.GetProductState”Client.GetProductState(name: string) → ProductStateTypes: ProductState
The state of a declared pass or product for the local player, as one
Scribe.ProductState string: "purchasable", "owned", "paid-random-restricted",
"policy-pending" or "not-loaded". The same answer the server’s
Server.GetProductState gives, computed here from the mirror and the local
player’s own policy, so a shop can grey a button without a round trip. Raises
for a name that is not declared.
.PromptPurchase
Section titled “.PromptPurchase”Client.PromptPurchase(name: string) → boolean string? table?Calls Server.PromptPurchase for the requesting player and returns its result.
The server resolves the name, checks ownership and paid-random policy, and
opens Roblox’s purchase prompt. No custom command is needed.
true means the prompt call succeeded, not that the player bought anything.
Receipts and grants stay on the server. Refusals match Server.PromptPurchase;
request failures additionally return Scribe.RequestFailed in the third slot.
Overlapping requests are refused, and the server blocks a second prompt while
one is open. Busy calls return false, "purchase prompt already open".
Edit mode returns false, "edit-mode", Scribe.RequestFailed without prompting.
A timeout means the result is unknown: the server may already have opened the
prompt even though its reply did not arrive. This call never retries itself.
Client.Stop cannot cancel a request that has already been sent.
local opened, reason, failed = Data.PromptPurchase("CoinBag")if not opened then warn(reason)end.ObserveProductState
Section titled “.ObserveProductState”Client.ObserveProductState(name: string, callback: (state: ProductState) -> ()) → () -> ()Types: ProductState
Calls callback with the current Client.GetProductState and again whenever
it changes: the profile loading, the policy landing, ownership changing.
Returns a disconnect.
.GetGiftCredits
Section titled “.GetGiftCredits”Client.GetGiftCredits() → { [string]: number }The local player’s unassigned gift credits by product name.
.GetPurchases
Section titled “.GetPurchases”Client.GetPurchases(filter: PurchaseFilter?) → { table }Types: PurchaseFilter
The local player’s purchase history for history UI. Only returned if the
game opts into replicating purchase logs (PurchaseLog.ReplicateRobux).
PurchaseFilter is { Kind: ("Robux" | "InGame")?, Category: string?, ItemId: string?, Since: number?, Limit: number? }, exported as
Scribe.PurchaseFilter.
.GetSaveInfo
Section titled “.GetSaveInfo”Client.GetSaveInfo() → SaveInfoTypes: SaveInfo
The owner’s replicated save state ({ LastSaveAt, LastResult, Dirty, Size })
for “Saving… / Saved ✓” UI. Size is the approximate byte size of the last
successful save (nil before the first).
Client.Raw: ClientThe same Data object without the typed accessor tree: the untyped
escape hatch if the type solver ever trips on your template.
.ServerStore
Section titled “.ServerStore”Client.ServerStore: tableThe server-owned tree as this client mirrors it, present only when the
bundle declares ServerStore. Read and observe it like any other root,
with no player argument; writes belong on the server.
See Server-owned state.
Edit Mode
Section titled “Edit Mode”Client.Mock(values: { [string]: any }?, scribeState: { Perks: { string }?, GiftCredits: { [string]: number }?, PurchaseLogs: { Robux: { any }?, InGame: { any }? }?, Leaderboards: { [string]: { LeaderboardEntry } }? }?)Types: LeaderboardEntry
Seeds mock data for a storybook / edit mode. Errors outside edit mode. Use it to render Scribe-backed components with realistic state.
values holds template fields to seed and is deep-merged into the mirror,
so you only pass the fields your component reads. scribeState seeds the
monetization and leaderboard state those APIs read.
.MockCommand
Section titled “.MockCommand”Client.MockCommand(name: string, handler: (...any) -> ...any)Registers a fake handler so Client.Request resolves in edit mode without
a server. Errors outside edit mode.