Skip to content

The exported shapes that the signatures on the other API pages name. Every one of them is reachable through the module you required, so you can annotate your own locals with Scribe.LeaderboardEntry and get the same checking Scribe uses internally. Nothing here is something you construct by hand unless the entry says so.

FieldTypeWhat it is
ServerServerthe server-side data API (use on the server)
ClientClientthe client-side data API (use on the client)

What Scribe(options) returns. The same shared module is required on both sides; use .Server on the server and .Client on the client.

FieldTypeWhat it is
TemplateTyour data shape, built from plain values and declarators
ProfileStoreIndexstringthe DataStore name, which has no default
ProfileKeyPrefixstringper-player key prefix such as “PLAYER_”; "" means bare user-id keys

The table you hand Scribe(options). Only the three fields above are required, and every other option has a working default, so the smallest real configuration is three lines long.

Roughly fifty further options cover persistence, monetization, gifting, limits, integrity and diagnostics. Each is listed with its default, its accepted values and what happens when it is wrong in the Configuration reference, which is the source of truth for all of them. This entry deliberately does not restate that page.

FieldTypeWhat it is
AwaitBudget(requestType: string, count: number?, timeout: number?) -> (boolean, () -> ())pace a legacy read on the DataStore request budget

The third argument to ImportLegacyData and the fourth to OnPlayerInit, for pacing reads of the stores you are migrating away from.

AwaitBudget yields until count requests of that type are spare or timeout passes (default 120 seconds), then returns whether they were granted plus a release. Waiters are served first come, first served, per request type, and a queued one shows as "ImportThrottled" on Server.GetState.

Release once you stop asking, not when a read succeeds: the request counts either way. Retry on a fresh grant, never release before the call returns, and when in doubt do nothing. See Migrating to Scribe.

FieldTypeWhat it is
Namestringshown in transport diagnostics
MaxFrameBytesnumber?largest buffer this channel wants in one call; only ever lowers the budget
SendToClient(self, player: Player, bytes: buffer) -> ()server to one client
SendToAllClients((self, bytes: buffer) -> ())?optional broadcast fast path
ListenServer(self, callback: (player: Player, bytes: buffer) -> ()) -> ()server inbound
SendToServer(self, bytes: buffer) -> ()client to server
ListenClient(self, callback: (bytes: buffer) -> ()) -> ()client inbound
Release((self) -> ())?optional; Client.Stop calls it to disconnect what ListenClient connected

A replacement for Scribe’s built-in RemoteEvent channel, passed as Transport. See Custom transports.

type PlayerData<T> = ValueType<T>

One player’s typed accessor tree: the type of Data[player], Data.Get(player) and the client’s Data.Get(). Name it when you pass a player’s data into your own function, since a type function cannot be invoked across a module boundary.

type ServerData<T, = S = any> BuildServerDataType<T, Player, S>

The type of the .Server half of Bundle, for annotating a variable that holds it. Pass the server-store template as S to type its accessors too.

type ClientData<T, = S = any> BuildClientDataType<T, S>

The type of the .Client half of Bundle. Scribe.ServerOnly roots are absent from it. Pass the server-store template as S to type its visible accessors too.

type ScribeModule = typeof(Scribe) & <T, S>(options: ScribeOptions<T, S>) -> Bundle<T, S>

The module itself: every Scribe.* declarator and helper, plus the call signature that builds a Bundle. Name it when you pass the module around.

type Timed<T> = { __scribeTimed: T }

What Scribe.Timed returns, and the type a cooldown field carries in your template. You never build one of these yourself. Emberfall declares LastDaily = Scribe.Timed(86400), arms it with Value.SetTimed, and asks about it with Server.OnCooldown and Server.PeekCooldown. See Timers & Cooldowns.

type Flags = { string }

What a Scribe.Flags field reads back: the member names currently enabled, not the full set of declared members. Emberfall’s Settings = Scribe.Flags({ "Music", "Sfx", "Tips" }) reads back as { "Music", "Tips" } for a player who turned Sfx off. Flip one member with Value.Enable, Value.Disable or Value.Toggle, and test one with Value.Has.

type SetOf<T> = { T }

What a Scribe.SetOf field reads back: its members, deduplicated and in sorted order, so two profiles holding the same members hold the same table. A set has no positions, so you change it with Value.Add and Value.Remove rather than by index, and both return false when they would be a no-op. See Containers.

type ServerOnly<T> = { __scribeServerOnly: T }

Wraps a field so it never leaves the server. A Scribe.ServerOnly field is absent from the replicated tree entirely, so no part of it reaches the wire even inside a whole-subtree read, and a client cannot ask for it. Use it for anything an exploiter must not see, such as Emberfall’s anti-cheat counters. See Replication & Visibility.

type Derived<T> = { __scribeDerived: T }

What Scribe.Derived returns. A derived field is computed from other fields rather than stored, so it is read-only and Value.Set on one throws. Emberfall’s Level derives from Xp, recomputes whenever Xp changes, and replicates like any other field. See Derived Fields.

type MapOf<K,V> = { [K]: V }

What a Scribe.MapOf field reads back: the whole map, keyed by the type you declared. Unlike a dictionary the key set is open, so Emberfall’s Friends = Scribe.MapOf("integer", { ... }) accepts any user id. Add and remove keys with Value.Set and Value.Remove, and watch them with Value.OnKeyAdded and Value.OnKeyRemoved.

type MapKeyType = "integer" | "string"

The key type you name in the first argument to Scribe.MapOf. It is a declaration rather than a hint. A DataStore serializes every object key to a string, so an integer map would come back holding "123" where it stored 123; the declared type is what lets Scribe convert the keys back unambiguously on load.

What a Scribe.Big field reads back, and what its arithmetic returns. It is an object rather than a number, so data.Get().Wealth hands you something carrying .M, .E and a handful of methods. Its surface has its own page: see BigValue and Big Numbers.

FieldTypeWhat it is
Minnumber?lowest accepted value; a write below it clamps or rejects per BoundsPolicy
Maxnumber?highest accepted value
Precision(number | "f32")?opt in to a narrower wire form; omit to keep the full f64

The options table for Scribe.Number. Precision is the only thing that narrows the field on the wire, and declaring Min and Max alone does not imply it. Scribe.Int takes Min and Max but refuses Precision, because a bounded integer already packs from its own bounds and there would be nothing left for Precision to say.

FieldTypeWhat it is
Min(number | string)?lowest accepted value
Max(number | string)?highest accepted value

The options table for Scribe.Big. A bound past 1.8e308 has to be written as a numeric string, because a larger Lua literal is already math.huge and the template validator rejects that. Emberfall caps prestige currency with Scribe.Big(0, { Max = "1e400" }).

FieldTypeWhat it is
Precision"exact"?store and send the full-fidelity CFrame instead of the packed one

The options table for Scribe.CFrame. A CFrame packs lossily by default, which is what you want for a saved checkpoint or camera. "exact" is the only other value, because a CFrame has no intermediate widths to offer the way a float does.

FieldTypeWhat it is
MaxItemsnumber?cap on length; growth past it is refused unless Evict is set
Evict("Front" | "Back")?turn the cap into a rolling window, dropping from that end

The options table for Scribe.ArrayOf, and the only container options that include Evict. Without MaxItems the array grows unbounded, which is how a profile quietly outgrows its DataStore budget. Emberfall keeps a rolling combat log with Scribe.ArrayOf(Scribe.String(""), { MaxItems = 50, Evict = "Front" }). See Containers.

FieldTypeWhat it is
MaxItemsnumber?cap on membership; growth past it is refused

The options table for Scribe.SetOf. The cap always refuses rather than evicting, and Evict itself is rejected as an unknown option here, because a set has no oldest member to drop.

FieldTypeWhat it is
MaxKeysnumber?cap on how many keys the container may hold
MaxKeyLengthnumber?cap on the byte length of any one key, string keys only

The options table for Scribe.DictOf and Scribe.MapOf. Both caps matter because keys are attacker-controlled the moment a client command can name one, and an unbounded key set is the quickest way past a profile’s size limit. MaxKeyLength counts bytes rather than characters, and an integer Scribe.MapOf refuses it outright, where it would read as a cap that does nothing.

type CooldownOptions = { IncludeOfflineTime: boolean? }

The optional second argument to Server.OnCooldown. Set IncludeOfflineTime = false for a cooldown that only ticks while the player is in the server.

type NumberMeta = { Min: number?, Max: number? }

The bounds table accepted by Scribe.Int and read back from a field as Value.Min and Value.Max.

type StringMeta = { MaxLength: number? }

The options table accepted by Scribe.String.

FieldTypeWhat it is
Cost{ Path: string, Amount: number }the numeric field to debit, and by how much
ItemIdstringwhat is being bought; recorded on the log entry
Categorystring?a grouping label of your choosing, for filtering later
Grant((data) -> ())?runs inside the same transaction as the debit
Meta{ [string]: any }?extra fields stored with the log entry
IdempotencyKeystring?max 64 bytes; two calls carrying the same key apply once

The table you hand Server.Purchase for a soft-currency purchase. The debit and the Grant either both happen or neither does, so a Grant that throws leaves the balance exactly where it was. A refused purchase claims nothing, which is why retrying after one still works even with a key. See Monetization.

FieldTypeWhat it is
Kind("Robux" | "InGame")?Robux receipts, soft-currency purchases, or both when omitted
Categorystring?match the Category recorded on the purchase
ItemIdstring?match a single item id
Sincenumber?only entries at or after this Unix timestamp, in seconds
Limitnumber?the most recent N matches

The optional filter you pass Server.GetPurchases or Client.GetPurchases. The fields combine with AND, so omitting all of them returns the whole retained log, which is itself capped. See Monetization.

FieldTypeWhat it is
Flow("Source" | "Sink")?default: Increment is a Source, Decrement is a Sink
TransactionType(Enum.AnalyticsEconomyTransactionType | string)?default “Gameplay”
ItemSkustring?what was bought or awarded
Currencystring?override the logged currency label; defaults to the field’s name
Fields{ [string]: any }?values for the custom fields you declared in Economy

The tag you pass Value.Increment or Value.Decrement to have Scribe emit a Roblox economy event alongside the write. Every field is optional and an untagged Increment emits nothing, so you opt in per call rather than per field. Source and Item are still accepted as aliases of TransactionType and ItemSku. See Economy Analytics.

FieldTypeWhat it is
Idnumberthe developer product id
Categorystring?purchase-log category, for Server.GetPurchases
Grant((data) -> ())?applies the purchase; runs inside a transaction, so it must not yield
Grantsstring?name of a perk to set instead of, or as well as, running Grant
PaidRandomboolean?a paid random item, refused on every purchase path for a player whose policy restricts them

One entry in the Products table. See Monetization.

FieldTypeWhat it is
Idnumberthe game pass id
Categorystring?purchase-log category, for Server.GetPurchases

One entry in the Passes table. The pass name is its own ownership key, so Server.Owns answers for it with no extra declaration.

FieldTypeWhat it is
Ranknumber1 for the top entry
UserIdnumberthe player’s user id
Namestringthe username recorded when the score was written
Score(number | BigScore)a big board hands back the big itself, not the packed key

One row of the array Server.GetLeaderboard and Client.GetLeaderboard return. Name is a snapshot rather than a live lookup, so a player who has since changed their username shows the old one until their score is written again. See Leaderboards.

type LeaderboardStatus = "Starting" | "Healthy" | "Degraded"

Health of one board, independent of profile persistence health.

FieldTypeWhat it is
StatusLeaderboardStatusStarting before a refresh succeeds or a failure is reported
PendingWritesnumberlatest scores waiting to be written, including rejected scores
RetryingWritesnumberpending scores whose last write failed
RejectedWritesnumberpermanently rejected scores waiting for a new value
LastReadAtnumber?Unix timestamp of the last successful refresh
ReadAgenumber?seconds since the last successful refresh
LastWriteAtnumber?Unix timestamp of the last successful write
LastReadErrorstring?current refresh failure
LastWriteErrorstring?latest write failure

Returned by Server.GetLeaderboardStatus. Reading it makes no requests.

FieldTypeWhat it is
Statstringdotted path to the field being ranked
Limitnumber?entries to keep (default 100)
Scalenumber?multiplier applied before the score is stored as an integer
SigFigsnumber?Scribe.Big stats only: ranking-key significant figures, 1 to 15 (default 12)
Replicateboolean?default false (server-only); true streams the board to clients
RefreshIntervalnumber?approximate seconds between reads (default 60, floor 60, jittered)
WriteIntervalnumber?minimum seconds between changed-score writes per player/store (default 30, minimum 1); ignored for Server scope
StoreNamestring?override the OrderedDataStore name (default “LB_”); a periodic board appends the period
Period("Daily" | "Weekly")?a board that resets on the bundle’s PeriodReset clock; nil is lifetime
Mode("Gain" | "Peak")?with Period: rank the gain since it began (default) or the peak seen in it
Scope("Global" | "Server")?“Server” ranks the players on this server only, with no store, no request cost, and a 5 second default refresh

One entry in the Leaderboards table. See Leaderboards.

FieldTypeWhat it is
Path{ string }the exchangeable path, a top-level field or container
Kind("Key" | "Qty" | "Stack")whole entry, an amount, or part of a stacked entry
Keyany?Key and Stack: the container key, set member or array value
Amountnumber?Qty and Stack: how much to move

One item in a basket handed to [Server.Exchange.Attempt], naming what that player GIVES. Every Path must be declared in the Exchangeable option, and a leg is checked against that list by path AND by kind before any value moves. See Exchange.

FieldTypeWhat it is
Path{ string }the path the value came from, or is going to
Kindstring“Key”, “Qty” or “Stack”
Keyany?the container key, where the leg has one
Amountnumber?how much, for a Qty or Stack leg

One leg of what a player has staked or is owed, as reported by [Server.Exchange.Open]. Freshly built on every call and aliases nothing in Scribe’s ledger, so it is safe to keep or mutate.

FieldTypeWhat it is
Idstringthe exchange id, which the operator verbs take
State("Claimed" | "Staked" | "Delivering")how far this exchange got
Partnernumber?the counterparty’s UserId
Staked{ OpenLeg }what this player handed over
Owed{ OpenLeg }what this player is owed
Sincenumber?Unix timestamp the exchange reached its current state

One open exchange, as reported by [Server.Exchange.Open]. Escrow takes value out of the balance on purpose and resolution is not time bounded, so Staked and Owed are how a game shows a player where their items went rather than leaving them looking at a hole. See Exchange.

FieldTypeWhat it is
Path{ string }the container or balance a leg may move
Kind"Key" | "Qty" | "Stack"whole entry, an amount off a balance, or part of a stack
Countstring?Stack only: the field holding the count
Identity{ string }?fields that decide whether two stack entries are the same item
Ignore{ string }?fields excluded from that identity check

One entry in the Exchangeable allowlist. Nothing moves through Server.Exchange unless a spec permits it. See Exchange.

FieldTypeWhat it is
Dirtybooleantrue when there are writes that have not reached the DataStore
LastSaveAtnumber?Unix timestamp of the last successful save, in seconds; nil before the first
LastResult("Ok" | "Fail")?how the last save attempt ended
Sizenumber?approximate bytes written by the last successful save

What Server.GetSaveInfo and Client.GetSaveInfo return. Size is the field worth watching: a profile creeping toward the DataStore limit shows up here long before a save actually starts failing. See Diagnostics.

FieldTypeWhat it is
Availablebooleanfalse when the engine could not be asked at all
Reasonstring?why not, when Available is false
Budgets{ [string]: number }remaining requests, keyed by DataStoreRequestType name
AtnumberUnix timestamp when the snapshot was taken, in seconds

What Scribe.GetBudgetSnapshot returns. It is resolved per call and never cached, and a server with no DataStoreService at all, such as a simulated one, reports Available false rather than raising. See Diagnostics.

FieldTypeWhat it is
AtnumberUnix timestamp when the entry was recorded, in seconds
LevelLogLevelDebug through Fatal
CategoryLogCategorywhich subsystem emitted it
CodeLogCodethe stable identifier you branch on
Messagestringhuman-readable text, not stable across versions
Context{ [string]: any }?structured detail; the keys vary by Code

One record from Scribe.GetRecentLogs, and the single argument every sink registered with Scribe.AddLogSink receives. Branch on Code, never on Message. See Diagnostics.

type LogLevel = "Debug" | "Info" | "Warn" | "Error" | "Fatal"

How severe a LogEntry is. LogLevel filters the console and default sinks; LogRingLevel controls retained history. A sink can set its own Level.

FieldTypeWhat it is
LevelLogLevel?minimum severity; defaults to the configured LogLevel
MaxQueuednumber?waiting entries per sink, 1 to 4096; default 128

Options for Scribe.AddLogSink. One callback runs at a time per sink.

FieldTypeWhat it is
BundleIdnumberstable within this process; distinguishes bundles
Namestringconfigured board name
StatusLeaderboardStatuscurrent board health
PendingWritesnumberqueued, in-flight and rejected scores
RetryingWritesnumberpending scores whose last write failed
RejectedWritesnumberrejected scores waiting for a changed value
LastReadAtnumber?Unix timestamp of the last successful refresh
ReadAgenumber?seconds since that refresh
LastWriteAtnumber?Unix timestamp of the last successful write
LastReadErrorstring?current refresh failure
LastWriteErrorstring?latest write failure

One record from Scribe.GetLeaderboardSnapshot.

type LogCategory = "Persistence" | "Replication" | "Transport" | "Commands" | "Leaderboards" | "Monetization" | "Gifting" | "Integrity" | "Lifecycle" | "Derived"

Which subsystem emitted a LogEntry. A sink added with Scribe.AddLogSink usually filters on this rather than on individual codes, so you can route Persistence somewhere loud and Replication somewhere quiet.

The stable identifier on a LogEntry, such as PROFILE_LOAD_FAILED. There are over 150 of them, and every one is listed with its meaning, its severity and what to do about it in the Log Code Reference. This is the field to branch on. Message is prose and is free to change between versions.

FieldTypeWhat it is
Resolve((player: Player) -> { [string]: any })?ambient values shared across every currency
Prefixboolean?format custom-field values as “Team - Red” (default true)
Currencies{ [string]: EconomyCurrencyConfig }?per-currency overrides
LogEconomyEventEconomyLogFn?advanced/test seam; defaults to AnalyticsService

The Economy option. Declares how a tagged Value.Increment reaches Roblox’s economy dashboard. See Economy Analytics.

FieldTypeWhat it is
Labelstring?logged currency name (default: the field’s name)
Fields{ EconomyFieldSpec }?up to three dimensions, mapped to CustomField01/02/03 in order
Resolve((player: Player) -> { [string]: any })?ambient values for this currency, merged over the global Resolve

One entry in EconomyConfig’s Currencies table.

type EconomyFieldSpec = string | { Name: string, Prefix: boolean? }

One custom-field dimension in EconomyCurrencyConfig. A bare string names the field; the table form also turns the “Label - Value” prefix off for that field.

type EconomyLogFn = (userId: number, flowType, currency: string, amount: number, endBalance: number, transactionType: string, itemSku: string?, fields: { [string]: any }?) -> ()

The seam EconomyConfig uses to reach AnalyticsService:LogEconomyEvent. Replace it in tests; leave it alone in a live game.

type Status = "Healthy" | "Degraded" | "Outage"

Scribe’s view of whether DataStores are answering, from Scribe.GetStatus, Client.GetServiceStatus and Scribe.OnStatusChanged.

FieldTypeWhat it is
Kind"Init" | "Set" | "Insert" | "Remove" | "Clear"what the change did
Path{ string | number }the field it landed on, from the root
Valueunknown?the new value, absent for Remove and Clear
Indexnumber?array position, for Insert and Remove

One replicated change, as the debug hook and the Studio plugin’s change feed see it. You never build one; it is the shape handed to a stream digest.

type LoadPhase = "WaitingForErase" | "AcquiringSession" | "ReadingProfile" | "Retrying" | "Initializing" | "Importing" | "ImportThrottled"

The stage of a pending load, returned by Server.GetState.

type LifecycleReason = "load-failed" | "migration-failed" | "erasing" | "session-ended" | "player-left" | "shutdown" | "still-loading" | "timeout"

Why a data session ended, or why waiting for data produced no tree. There is one value per state so you can branch exhaustively. It is the second return of Server.WaitForData, the argument Server.SessionEnded fires with, and the reason Server.GetState stores. The names are also on Scribe.Reason if you would rather not spell the strings. See Session Lifecycle.

type RequestReason = "rate-limited" | "not-ready" | "unknown-command" | "bad-args" | "bad-idempotency-key" | "error" | "reply-encode-failed" | "timeout" | "send-failed" | "edit-mode" | "mock-error"

Why Scribe itself refused a Client.Request. The last four are produced on the client and never reach the server. A handler that throws yields “error”, and its traceback goes to the COMMAND_ERROR log rather than to the client, so an exploiter learns nothing from a crash. The names are also on Scribe.RequestReason. See Commands & Requests.

type PurchaseReason = "player data not loaded" | "invalid Cost spec" | "invalid Cost amount" | "invalid cost path" | "cost path is not a spendable number" | "insufficient funds" | "paid random items are not available for this account" | "cannot check account settings right now; try again in a moment"

The fixed refusals of Server.Purchase. They are not exhaustive over the second return: a Grant that throws passes its own error text through instead. Test for "insufficient funds" specifically rather than assuming any other string is one of these. The names are also on Scribe.PurchaseReason.

The fixed refusals of Server.PromptGift. Each one is a finished sentence you can show a player unaltered: "buyer data not loaded", "invalid recipient", "cannot gift yourself", "gift cooldown", "product retired", "too many pending gifts", "recipient already owns this", "data services are experiencing issues; try again later", "could not reserve gift credit; try again later", "could not deliver gift; try again later", "gift delivery could not be confirmed; do not send it again", "could not record gift intent; try again later", "a gift of this item is already pending; try again shortly", "paid random items are not available for this account" and "cannot check account settings right now; try again in a moment". Two further refusals interpolate a value and so cannot be members. The names are also on Scribe.GiftReason. See Gifting.

The last two delivery refusals are not interchangeable. DeliveryFailed means the gift provably did not go out and a spent credit has been handed back, so a retry is the right advice. DeliveryUnconfirmed means the delivery write may have landed with only its answer lost: the credit stays spent on purpose, and a retry would send a SECOND gift under an id the recipient cannot dedupe. Show them as different outcomes.

type ProductState = "purchasable" | "owned" | "paid-random-restricted" | "policy-pending" | "not-loaded"

The state of a declared pass or product for one player, from Server.GetProductState and Client.GetProductState. Every value but "purchasable" is also the reason Server.PromptPurchase refuses with, so a shop and the prompt cannot disagree. The names are also on Scribe.ProductState.