Skip to content

Types

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.

Setup

Bundle

Field Type What it is
Server Server the server-side data API (use on the server)
Client Client the 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.

ScribeOptions

Field Type What it is
Template T your data shape, built from plain values and declarators
ProfileStoreIndex string the DataStore name, which has no default
ProfileKeyPrefix string per-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.

Declarators

Timed<T>

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.

Flags

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.

SetOf<T>

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.

ServerOnly<T>

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.

Derived<T>

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.

MapOf<K,V>

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.

MapKeyType

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.

BigValue

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.

Declarator options

FloatMeta

Field Type What it is
Min number? lowest accepted value; a write below it clamps or rejects per BoundsPolicy
Max number? 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.

BigMeta

Field Type What 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" }).

CFrameMeta

Field Type What 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.

ArrayOpts

Field Type What it is
MaxItems number? 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.

SetOpts

Field Type What it is
MaxItems number? 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.

DictOpts

Field Type What it is
MaxKeys number? cap on how many keys the container may hold
MaxKeyLength number? 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.

Monetization

PurchaseSpec

Field Type What it is
Cost { Path: string, Amount: number } the numeric field to debit, and by how much
ItemId string what is being bought; recorded on the log entry
Category string? 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
IdempotencyKey string? 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.

PurchaseFilter

Field Type What it is
Kind ("Robux" | "InGame")? Robux receipts, soft-currency purchases, or both when omitted
Category string? match the Category recorded on the purchase
ItemId string? match a single item id
Since number? only entries at or after this Unix timestamp, in seconds
Limit number? 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.

EconomyMeta

Field Type What it is
Flow ("Source" | "Sink")? default: Increment is a Source, Decrement is a Sink
TransactionType (Enum.AnalyticsEconomyTransactionType | string)? default "Gameplay"
ItemSku string? what was bought or awarded
Currency string? 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.

Leaderboards

LeaderboardEntry

Field Type What it is
Rank number 1 for the top entry
UserId number the player's user id
Name string the 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.

Diagnostics

SaveInfo

Field Type What it is
Dirty boolean true when there are writes that have not reached the DataStore
LastSaveAt number? Unix timestamp of the last successful save, in seconds; nil before the first
LastResult ("Ok" | "Fail")? how the last save attempt ended
Size number? 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.

BudgetSnapshot

Field Type What it is
Available boolean false when the engine could not be asked at all
Reason string? why not, when Available is false
Budgets { [string]: number } remaining requests, keyed by DataStoreRequestType name
At number Unix 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.

LogEntry

Field Type What it is
At number Unix timestamp when the entry was recorded, in seconds
Level LogLevel Debug through Fatal
Category LogCategory which subsystem emitted it
Code LogCode the stable identifier you branch on
Message string human-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.

LogLevel

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

How severe a LogEntry is. The LogLevel option sets the floor, and anything below it is never recorded at all, so raising it is the cheapest way to quieten a noisy production log. See Configuration.

LogCategory

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.

LogCode

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.

Reasons

LifecycleReason

type LifecycleReason = "load-failed" | "migration-failed" | "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.

RequestReason

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.

PurchaseReason

type PurchaseReason = "player data not loaded" | "invalid Cost spec" | "invalid Cost amount" | "invalid cost path" | "cost path is not a spendable number" | "insufficient funds"

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.

GiftReason

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", "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", and "a gift of this item is already pending; try again shortly". 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.