Scribe
The module you require and call. Scribe(options) compiles your template
and returns a { Server, Client } bundle. Use .Server on the server and
.Client on the client from the same shared module. Every option the table
accepts is documented in Configuration.
In edit mode (RunService:IsRunning() false, as in a UI Labs / Hoarcekat
storybook or the command bar) there is no server to build, so the bundle holds
the edit-mode client half and .Server errors if touched.
Scribe also exposes the field declarators (Scribe.Int, Scribe.Vector3,
…) you use inside a template, the visibility wrappers (Scribe.ServerOnly,
Scribe.Shared, Scribe.Session), and library-wide diagnostics.
local Scribe = require(ReplicatedStorage.Packages.Scribe)return Scribe({ Template = { Coins = Scribe.Int(0, { Min = 0 }) }, ProfileStoreIndex = "PlayerData", ProfileKeyPrefix = "PLAYER_",})Find a member
Section titled “Find a member”Choose a group, then follow a member link for its signature and behavior.
Setup
Visibility
ServerOnly · Shared · Session · Visibility
Field Types
Int · Number · Big · String · Enum · Flags · Timed · Dynamic · Derived · Optional · ArrayOf · SetOf · MapOf · DictOf · CFrame · Datatypes · Vector3 · Vector2 · Vector3int16 · Vector2int16 · Color3 · BrickColor · UDim · UDim2 · Rect · NumberRange · NumberSequence · ColorSequence · DateTime · EnumItem · Font · PhysicalProperties
Lifecycle
Reason · SessionState · LoadPhase
General
Configuration
Commands
Monetization
PurchaseReason · GiftReason · ProductState
Diagnostics
OpKind · LogLevel · LogCategory · Status · GetProfileKeyPrefix · GetStatus · GetLeaderboardSnapshot · OnStatusChanged · OnIssue · AddLogSink · RegisterAddon · GetRecentLogs · GetMetrics · GetPercentiles · GetBudgetSnapshot
Leaderboards
Reasons
.Version
Section titled “.Version”Scribe.Version: stringThe library version (e.g. "1.0.0").
Scribe.new(options: ScribeOptions) → BundleTypes: ScribeOptions, Bundle
Compiles the template and builds the { Server, Client } bundle. Calling the
module directly (Scribe(options)) is sugar for this. Require the same
shared module on both the server and the client and use the matching half.
options is a ScribeOptions table. Only three fields are required; the rest
cover persistence, monetization, gifting, behaviour limits and diagnostics.
Configuration documents every one of them, and
The three you must set is where
to start on a first bundle.
Visibility
Section titled “Visibility”.ServerOnly
Section titled “.ServerOnly”Scribe.ServerOnly(value: T) → ServerOnly<T>Types: ServerOnly
Marks a root field server-only: it persists and is readable on the server, but is never replicated to any client. May also wrap a nested field to keep just that subtree server-side.
The field is absent from the client’s accessor type, so reading it there
is a type error rather than a nil at runtime. On the server it reads like
any other field.
Combines with Scribe.Session for runtime state only the server sees:
Scribe.ServerOnly(Scribe.Session(v)). Combining it with Scribe.Shared is
a startup error, since a field has one replication target, not two.
.Shared
Section titled “.Shared”Scribe.Shared(value: T) → TMarks a root field shared: it replicates to every client (not just the owner), for public info like a display name or team.
Combines with Scribe.Session for runtime state everyone sees and nothing
saves: Scribe.Shared(Scribe.Session(v)). Combining it with
Scribe.ServerOnly is a startup error.
.Session
Section titled “.Session”Scribe.Session(value: T) → TMarks a root field session-only: never persisted, and replicated to the
owner unless another wrapper says otherwise. Ideal for runtime state like
InCombat that should reset on rejoin.
Saving and replication are independent, so this composes with exactly one of
Scribe.ServerOnly (nobody sees it) or Scribe.Shared (everyone does), in
either order. Doubling it up is a startup error.
.Visibility
Section titled “.Visibility”Scribe.Visibility: { [string]: Visibility }The root visibilities, as named constants.
Field Types
Section titled “Field Types”Scribe.Int(default: number, meta: { Min: number?, Max: number? }?) → numberDeclares an integer field. Non-integer writes round (or reject under
BoundsPolicy = "Reject"); out-of-range writes clamp. Bounded ints also
pack to a smaller wire width.
.Number
Section titled “.Number”Scribe.Number(default: number, meta: FloatMeta?) → numberTypes: FloatMeta
Declares a floating-point field with optional bounds and an optional narrowed wire form.
Min and Max must be finite, since math.huge and NaN would each declare a
bound that silently checks nothing. For a bound past the double range use
Scribe.Big, whose bounds may be written as numeric strings.
Precision is opt-in and narrows only the replicated copy; the server keeps and
persists the full double either way. A fixed-point step requires both Min and
Max, and bounds too wide for the width you ask for refuse to compile.
Health = Scribe.Number(100, { Min = 0, Max = 100, Precision = 0.5 }), -- 1 byteSee Narrowing a float for the byte table.
Scribe.Big(default: (number | string)?, meta: BigMeta?) → BigValueA number with unlimited range, for an idle or simulator currency that runs past
2^53. It carries about 15 significant digits at any magnitude, so
1e20 + 1 == 1e20; use Scribe.Int when every digit has to be exact.
Essence = Scribe.Big(0, { Min = 0, Max = "1e600" }), -- a bound past 1.8e308 must be a stringdata.Essence.Get():Short() --> "1.95Dc"Set and every arithmetic method take a plain number, a numeric string, or
another big value, and Value.Multiply and Value.Divide exist only here.
Numeric strings, including defaults and bounds, are limited to 256 bytes before
whitespace removal. Longer strings are rejected, never truncated.
Get() returns a BigValue, not a plain number, and it will not
compare against one: Get() < 2000 throws and Get() == 5 is silently false.
A big field may be a leaderboard stat, which requires non-negative values and
refuses Scale. See Big Numbers and
Leaderboards.
.String
Section titled “.String”Scribe.String(default: string, meta: { MaxLength: number? }?) → stringDeclares a string field, optionally length-capped.
MaxLength is a byte budget and must be a non-negative integer; a template
declaring otherwise fails to compile. Over-long values are truncated on a
character boundary (never mid-character, which would produce invalid UTF-8 and
fail the profile’s next save), or refused outright under
BoundsPolicy = "Reject".
Scribe.Enum(default: string, members: { string }) → stringDeclares a string field restricted to a fixed set of members. Writes outside the set are rejected, and the value packs to a single byte.
.Flags
Section titled “.Flags”Scribe.Flags(members: { string }) → FlagsTypes: Flags
A fixed set of named booleans stored as one field, up to 32 members, addressed by name:
Settings = Scribe.Flags({ "Music", "Sfx", "TutorialDone" }),data.Settings.Enable("Music")data.Settings.Has("Music") --> trueThe accessors are Value.Enable, Value.Disable, Value.Toggle, Value.Has,
Value.Count and Value.Clear. Members are not accessor children, so it is
Settings.Enable("Music") and not Settings.Music.Set(true). Enabling an
undeclared name is an error, and setting a flag to what it holds writes nothing.
The stored value is the enabled member names, so reordering or removing members later is safe, and more than 32 is refused when the template compiles. See Named booleans.
.Timed
Section titled “.Timed”Scribe.Timed(default: T) → TDeclares a field with an expiry. Set it with field.SetTimed(value, seconds);
it auto-clears back to default when the timer lapses (firing Changed).
Great for boosters and temporary buffs.
.Dynamic
Section titled “.Dynamic”Scribe.Dynamic(factory: () -> T) → TDeclares a field whose default is produced by factory per profile rather than
frozen once at server start. The field types as the factory’s return type, and a
datatype result is packed for you.
CreatedUnix = Scribe.Dynamic(os.time),JoinedAt = Scribe.Dynamic(function() return DateTime.now() end),The factory runs only to seed a brand-new profile, or an existing profile that gains the field after you add it, so a returning player keeps a stored value.
It must be pure, because Scribe also calls it once while your template module
loads to sample its return type, so anything that yields, errors or reserves an
id fires that effect with no profile attached. A factory takes no arguments, so
player-specific defaults belong in OnPlayerInit, and Scribe.Dynamic inside a
Scribe.Session root is a startup error.
.Derived
Section titled “.Derived”Scribe.Derived(output: T, inputs: { string }, compute: (...any) -> T) → TDeclares a field Scribe computes from other declared fields instead of accepting writes. It is never persisted, never migrated, recomputed whenever an input changes, and replicated only when the receiver cannot compute it itself.
output is an ordinary declarator supplying the type and bounds. inputs are
dotted paths to statically declared scalar fields, passed to compute in the
order written; one derived field may read another, and the compiler orders the
graph and rejects cycles at startup. compute must be pure, because Scribe calls
it once per realm at startup to seed the default.
Reads are ordinary (Get, Changed, Observe) and every mutator throws. The
field must be a root field or live inside a Scribe.Session root, and wrapping
it in Scribe.Session, Scribe.Timed, Scribe.Dynamic, Scribe.Optional or a
container is refused when the template compiles. See
Derived Fields.
.Optional
Section titled “.Optional”Scribe.Optional(inner: T) → T?Declares a field that may legitimately be absent, wrapping another declarator for its type and bounds:
Pets = Scribe.DictOf({ Species = Scribe.String("", { MaxLength = 32 }), Nickname = Scribe.Optional(Scribe.String("", { MaxLength = 20 })),}),The inner declarator’s default is dropped: an optional field has no default,
so it is not seeded into a new profile and not filled in when an element write
omits it. Get() returns nil until something writes it, and Set(nil) takes
it back to absent.
It wraps leaves only. Scribe.Timed, Scribe.Dynamic and the container
declarators are all refused inside it, since each of the three exists to supply
a value an optional field is defined not to have.
.ArrayOf
Section titled “.ArrayOf”Scribe.ArrayOf(shape: T, opts: ArrayOpts?) → { T }Types: ArrayOpts
Declares an array whose elements have a schema, so type checking, bounds and
datatype packing all apply per element. shape may be a record or a single leaf
declarator, and it nests freely.
PlacedFurniture = Scribe.ArrayOf({ Cf = Scribe.CFrame(CFrame.new()), ItemId = Scribe.String("", { MaxLength = 64 }),}, { MaxItems = 200, Evict = "Front" }),opts takes MaxItems and Evict and nothing else. MaxItems refuses growth
past the cap but leaves an already-oversized container writable, so a rolling
deploy is safe. Evict requires MaxItems and names the end an Insert at the
cap drops from, "Front" or "Back".
Element records are closed at every depth, and Value.Find, Value.Has and
Value.RemoveValue compare by value. See Containers.
.SetOf
Section titled “.SetOf”Scribe.SetOf(element: T, opts: SetOpts?) → SetOf<T>A collection of unique entries: membership, not order. Use it where you would
otherwise keep a dictionary of true values, such as owned items.
Unlocked = Scribe.SetOf(Scribe.String("", { MaxLength = 32 })),data.Unlocked.Add("AshfallRidge") --> truedata.Unlocked.Remove("AshfallRidge") --> trueThe accessors are Value.Add, Value.Remove, Value.Has, Value.Find,
Value.Count and Value.Clear. Remove takes the member itself and returns
whether it was one. An Add of a value already present, or a Remove of one
that is absent, writes nothing and fires no Changed.
Entries are stored deduplicated and sorted, so element must be a scalar and a
record shape is refused by the declarator. MaxItems caps membership and is the
only option. See Sets of unique values.
.MapOf
Section titled “.MapOf”Scribe.MapOf(keyType: MapKeyType, value: V, opts: DictOpts?) → MapOf<K, V>Types: MapKeyType, DictOpts, MapOf
A dictionary whose keys have a declared type, for keys that are user ids or
anything else you would otherwise tostring at every write and tonumber at
every read. Scribe.DictOf is the string-keyed shorthand.
Friends = Scribe.MapOf("integer", { Name = Scribe.String("") }),data.Friends[ava.UserId].Name.Set("Ava") -- no tostringkeyType is "integer" or "string", and anything else errors at declaration.
Integer keys come back as numbers even though a DataStore serializes every object
key to a string, and fractional, infinite and NaN keys are refused at the write.
opts takes MaxKeys and MaxKeyLength and nothing else, and MaxKeyLength
with keyType = "integer" is an error rather than a cap that could never fire.
See Maps with typed keys.
.DictOf
Section titled “.DictOf”Scribe.DictOf(shape: V, opts: { MaxKeys: number?, MaxKeyLength: number? }?) → { [string]: V }Declares a string-keyed dictionary whose values have a schema, the dictionary
counterpart of Scribe.ArrayOf:
Inventory = Scribe.DictOf({ Count = Scribe.Int(1, { Min = 1, Max = 999 }) }, { MaxKeys = 200 }),data.Inventory.Emberblade.Count.Get() -- nil: nothing has written this keydata.Inventory.Emberblade.Count.Increment(5) -- 6: starts from the declared defaultA key exists only once something writes it; until then Get() is nil and
Count() does not include it. Any string is a valid key, so a typo is a new key
rather than an error. Use plain declared fields when the key set is fixed.
MaxKeys caps the key count and MaxKeyLength caps each key’s byte length. Both
are write errors, and MaxKeyLength rejects rather than truncates. The element
rules match Scribe.ArrayOf; see Containers.
.Vector3
Section titled “.Vector3”Scribe.Vector3(default: Vector3) → Vector3Declares a Roblox datatype field. The value is stored and replicated as a
compact packed buffer; Get/Set convert at the boundary, so your code only
ever sees the real Vector3.
.Vector2
Section titled “.Vector2”Scribe.Vector2(default: Vector2) → Vector2Declares a Roblox datatype field. The value is stored and replicated as a
compact packed buffer; Get/Set convert at the boundary, so your code only
ever sees the real Vector2.
.Vector3int16
Section titled “.Vector3int16”Scribe.Vector3int16(default: Vector3int16) → Vector3int16Declares a Roblox datatype field. The value is stored and replicated as a
compact packed buffer; Get/Set convert at the boundary, so your code only
ever sees the real Vector3int16.
.Vector2int16
Section titled “.Vector2int16”Scribe.Vector2int16(default: Vector2int16) → Vector2int16Declares a Roblox datatype field. The value is stored and replicated as a
compact packed buffer; Get/Set convert at the boundary, so your code only
ever sees the real Vector2int16.
.Color3
Section titled “.Color3”Scribe.Color3(default: Color3) → Color3Declares a Roblox datatype field. The value is stored and replicated as a
compact packed buffer; Get/Set convert at the boundary, so your code only
ever sees the real Color3.
.BrickColor
Section titled “.BrickColor”Scribe.BrickColor(default: BrickColor) → BrickColorDeclares a Roblox datatype field. The value is stored and replicated as a
compact packed buffer; Get/Set convert at the boundary, so your code only
ever sees the real BrickColor.
Scribe.UDim(default: UDim) → UDimDeclares a Roblox datatype field. The value is stored and replicated as a
compact packed buffer; Get/Set convert at the boundary, so your code only
ever sees the real UDim.
.UDim2
Section titled “.UDim2”Scribe.UDim2(default: UDim2) → UDim2Declares a Roblox datatype field. The value is stored and replicated as a
compact packed buffer; Get/Set convert at the boundary, so your code only
ever sees the real UDim2.
Scribe.Rect(default: Rect) → RectDeclares a Roblox datatype field. The value is stored and replicated as a
compact packed buffer; Get/Set convert at the boundary, so your code only
ever sees the real Rect.
.NumberRange
Section titled “.NumberRange”Scribe.NumberRange(default: NumberRange) → NumberRangeDeclares a Roblox datatype field. The value is stored and replicated as a
compact packed buffer; Get/Set convert at the boundary, so your code only
ever sees the real NumberRange.
.NumberSequence
Section titled “.NumberSequence”Scribe.NumberSequence(default: NumberSequence) → NumberSequenceDeclares a Roblox datatype field. The value is stored and replicated as a
compact packed buffer; Get/Set convert at the boundary, so your code only
ever sees the real NumberSequence.
.ColorSequence
Section titled “.ColorSequence”Scribe.ColorSequence(default: ColorSequence) → ColorSequenceDeclares a Roblox datatype field. The value is stored and replicated as a
compact packed buffer; Get/Set convert at the boundary, so your code only
ever sees the real ColorSequence.
.DateTime
Section titled “.DateTime”Scribe.DateTime(default: DateTime) → DateTimeDeclares a Roblox datatype field. The value is stored and replicated as a
compact packed buffer; Get/Set convert at the boundary, so your code only
ever sees the real DateTime.
.EnumItem
Section titled “.EnumItem”Scribe.EnumItem(default: EnumItem) → EnumItemDeclares a Roblox datatype field. The value is stored and replicated as a
compact packed buffer; Get/Set convert at the boundary, so your code only
ever sees the real EnumItem.
Scribe.Font(default: Font) → FontDeclares a Roblox datatype field. The value is stored and replicated as a
compact packed buffer; Get/Set convert at the boundary, so your code only
ever sees the real Font.
.PhysicalProperties
Section titled “.PhysicalProperties”Scribe.PhysicalProperties(default: PhysicalProperties) → PhysicalPropertiesDeclares a Roblox datatype field. The value is stored and replicated as a
compact packed buffer; Get/Set convert at the boundary, so your code only
ever sees the real PhysicalProperties.
.CFrame
Section titled “.CFrame”Scribe.CFrame(default: CFrame, meta: CFrameMeta?) → CFrameTypes: CFrameMeta
Declares a CFrame field. An axis-aligned rotation, the common case for placed
structures, packs to 13 bytes; any other rotation packs as a quaternion in 29
and reads back turned by roughly 1e-7.
Precision = "exact" is the only option and it is off by default. It stores all
twelve components as f32 at a flat 49 bytes, so the value round-trips bit for
bit. Reach for it when the orientation is data you compare, accumulate or replay.
"exact" is its only accepted value, and Precision on any other datatype
declarator is refused when the template compiles.
Camera = Scribe.CFrame(CFrame.identity, { Precision = "exact" }), -- 49 bytes, losslessSee Full-precision CFrames for the byte table and for the axis snapping the 13-byte path does.
.Datatypes
Section titled “.Datatypes”Scribe.Datatypes: tableLow-level helpers for the 17 Roblox datatypes, exposed so a migration can convert a legacy representation into the packed form.
| Call | Returns |
|---|---|
Pack(name, value, precision?) | the packed buffer |
Unpack(name, b) | the datatype |
IsSupported(name) | whether Scribe has a codec for name |
PrecisionValues(name) | the Precision values name accepts, sorted, or nil |
Pack and Unpack both throw Scribe: unsupported datatype "<name>" on a name
with no codec, so ask IsSupported first when the name is not a literal.
precision is optional and additive: omit it and every datatype packs exactly the
bytes it always has. PrecisionValues reports what a datatype accepts, and today
only Scribe.CFrame accepts anything ("exact"); any other value throws naming
the ones it does. Unpack takes none, because every layout is self-describing
from its first byte, which is what lets old data keep decoding.
Lifecycle
Section titled “Lifecycle”.Reason
Section titled “.Reason”Scribe.Reason: { [string]: LifecycleReason }Types: LifecycleReason
Every value Server.WaitForData and the
SessionEnded signal can report, as a frozen table, so you can branch on a
constant instead of a hand-typed string:
local data, reason = Data.WaitForData(player)if not data and reason ~= Scribe.Reason.PlayerLeft then warn("data unavailable:", reason) -- PlayerLeft is routine, not an errorendThe seven members are LoadFailed, MigrationFailed, SessionEnded,
PlayerLeft, Shutdown, StillLoading and Timeout. Each one’s string, what it
means and whether it is worth retrying are in the
Session Lifecycle guide.
The matching Scribe.LifecycleReason type is the same set as a string union,
so a comparison against a typo fails to type-check.
.SessionState
Section titled “.SessionState”Scribe.SessionState: { [string]: SessionState }A profile’s session states, as named constants. See Server.GetState.
.LoadPhase
Section titled “.LoadPhase”Scribe.LoadPhase: { [string]: LoadPhase }Types: LoadPhase
The loading stages returned as the second value from Server.GetState.
General
Section titled “General”.Short
Section titled “.Short”Scribe.Short(value: number | BigValue, decimals: number?) → stringTypes: BigValue
Renders a quantity the short way a player reads it: 1.5K, 100M, 2.4B.
local label = Scribe.Short(data.Coins.Get()) -- "1.5M"Takes a plain number or a Scribe.Big value, so a balance label does not have to
know which numeric type the field was declared as.
decimals defaults to 2 and rounds rather than truncates, so Scribe.Short(1500, 0)
gives "2K". Anything past the suffix table renders in full rather than losing
its magnitude.
.SetShortSuffixes
Section titled “.SetShortSuffixes”Scribe.SetShortSuffixes(suffixes: { string })Replaces the suffix table Scribe.Short and BigValue:Short() render with.
Scribe.SetShortSuffixes({ "", "K", "M", "B", "T", "aa", "ab", "ac" })The defaults run K, M, B, T, Qa, Qi, Sx and on up. The conventions diverge past
T, so this exists rather than leaving a game to write its own formatter and stop
using this one. Index 1 is the units tier and must be empty, because it is what
renders a small number unchanged.
Configuration
Section titled “Configuration”.Configure
Section titled “.Configure”Scribe.Configure(config: { AutoSaveInterval: number? })Sets configuration that belongs to the process rather than to one bundle. Call it once, before constructing any bundle:
Scribe.Configure({ AutoSaveInterval = 60 })AutoSaveInterval is the autosave cadence, and it lives here because
ProfileStore’s AUTO_SAVE_PERIOD is a module-wide constant that also covers any
direct ProfileStore use in the same game. The per-bundle SaveInterval option
still wins where it is set, and two bundles asking for different cadences logs
SAVE_INTERVAL_CONFLICT. Values below 15 seconds clamp up to it.
Calling this twice with different values, or after a bundle exists, is an error, because the value may already have been applied.
Commands
Section titled “Commands”.RequestReason
Section titled “.RequestReason”Scribe.RequestReason: { [string]: RequestReason }Types: RequestReason
The reasons Scribe answers a Client.Request with, as named constants.
The eleven members are RateLimited, NotReady, UnknownCommand, BadArgs,
BadIdempotencyKey, Error, ReplyEncodeFailed, Timeout, SendFailed,
EditMode and MockError. Each one’s string and what provokes it are in the
Commands and Requests
guide.
A command handler may return any (false, string) of its own; those are
deliberately not members, so a reason that is not in this table came from your
handler rather than from Scribe.
.RequestFailed
Section titled “.RequestFailed”Scribe.RequestFailed: tableThe marker Client.Request returns as a third value whenever the refusal is
Scribe’s rather than your handler’s.
RequestReason tells you what a reason string means, but not who said it: a
handler may return any (false, string) it likes, including false, "timeout",
and those values reach the caller verbatim. Compare against this frozen table
instead; handler values cross the wire, which carries no reference identity, so
nothing a handler returns can ever equal it.
local ok, reason, failed = Data.Request("EquipItem", itemId)if failed == Scribe.RequestFailed then retryLater(reason) -- Scribe refused; `reason` is a RequestReasonelseif not ok then showToast(reason) -- your handler's own message, safe to showendIt is present on every framework refusal, including the four the client raises with no reply frame at all, and absent on success and on handler values.
Monetization
Section titled “Monetization”.PurchaseReason
Section titled “.PurchaseReason”Scribe.PurchaseReason: { [string]: PurchaseReason }Types: PurchaseReason
The fixed refusals of Server.Purchase. A Grant that throws passes its own
error text through instead, so a non-member reason is a failed grant.
The eight members are DataNotLoaded, InvalidCostSpec, InvalidCostAmount,
InvalidCostPath, CostPathNotSpendable, InsufficientFunds,
PaidRandomRestricted and PolicyPending. Each one’s
string is in the Monetization guide.
.GiftReason
Section titled “.GiftReason”Scribe.GiftReason: { [string]: GiftReason }Types: GiftReason
The fixed refusals of Server.PromptGift. Two further refusals are interpolated
with the offending value and so cannot be constants: an unknown product name, and
a failed purchase prompt.
The fourteen members are BuyerDataNotLoaded, InvalidRecipient,
CannotGiftYourself, GiftCooldown, TooManyPending, DataServicesDown,
RecipientAlreadyOwns, CreditReserveFailed, DeliveryFailed,
DeliveryUnconfirmed, AlreadyPending, IntentRecordFailed,
PaidRandomRestricted and PolicyPending. Each one’s
string is in the Gifting guide.
.ProductState
Section titled “.ProductState”Scribe.ProductState: { [string]: ProductState }Types: ProductState
The members are Purchasable, Owned, PaidRandomRestricted, PolicyPending
and NotLoaded. Each one’s string is in the
Monetization guide.
Diagnostics
Section titled “Diagnostics”.OpKind
Section titled “.OpKind”Scribe.OpKind: { [string]: OpKind }The kinds of replication op, as named constants.
.LogLevel
Section titled “.LogLevel”Scribe.LogLevel: { [string]: LogLevel }Types: LogLevel
The log levels, as named constants, for Scribe.GetRecentLogs filters.
.LogCategory
Section titled “.LogCategory”Scribe.LogCategory: { [string]: LogCategory }Types: LogCategory
The log categories, as named constants, for Scribe.GetRecentLogs filters.
.Status
Section titled “.Status”Scribe.Status: { [string]: Status }Types: Status
The DataStore health states, as named constants. See Scribe.GetStatus and Client.GetServiceStatus.
.GetProfileKeyPrefix
Section titled “.GetProfileKeyPrefix”Scribe.GetProfileKeyPrefix() → stringThe ProfileKeyPrefix the running server bundle was started with, or "PLAYER_" before
any and on the client. Add-ons use it to recognise profile keys in log text, so a custom
prefix is redacted as the default one is.
.GetStatus
Section titled “.GetStatus”Scribe.GetStatus() → "Healthy" | "Degraded" | "Outage"The current data-service health, derived from ProfileStore’s error signals
with hysteresis. Also broadcast to clients via Client:GetServiceStatus.
.GetLeaderboardSnapshot
Section titled “.GetLeaderboardSnapshot”Scribe.GetLeaderboardSnapshot() → { LeaderboardSnapshotEntry }Types: LeaderboardSnapshotEntry
Cached health for every board in running server bundles. Makes no DataStore
requests. Returns fresh records sorted by process-local BundleId, then Name.
Empty on clients and when no boards are running. Bundle IDs are not persistent.
.OnStatusChanged
Section titled “.OnStatusChanged”Scribe.OnStatusChanged: SignalFires with the new status ("Healthy" | "Degraded" | "Outage") whenever
service health changes.
The value is a Signal, where Connect, Once and Wait are documented.
.OnIssue
Section titled “.OnIssue”Scribe.OnIssue: SignalFires for every Error/Fatal log entry: the single hook to wire up developer alerting (Discord webhook, PagerDuty, …) from your own game code.
The value is a Signal, where Connect, Once and Wait are documented.
.AddLogSink
Section titled “.AddLogSink”Scribe.AddLogSink(fn: (entry: LogEntry) -> (), options: LogSinkOptions?) → () -> ()Types: LogEntry, LogSinkOptions
Registers an extra log sink. Scribe never sends logs anywhere itself. Add a sink to forward them to your own pipeline. Keep secrets in game code.
Filters at options.Level or the configured LogLevel before dispatch.
Each sink has one worker and a bounded queue (MaxQueued, default 128).
On overflow, more severe entries replace lower-severity entries; otherwise
the incoming entry is dropped. LogSinkDropped counts these losses.
The returned function detaches the sink and clears queued entries. A callback
already running may finish. Detaching twice is safe.
.RegisterAddon
Section titled “.RegisterAddon”Scribe.RegisterAddon(name: string, actions: { [string]: { Run: (args: any) -> any, Writes: boolean? } }) → () -> ()Registers actions an add-on offers to the Scribe Studio plugin, which lists them
and runs one on request. An action with Writes = true needs the plugin’s write
toggle. Registering the same name again replaces the earlier actions.
Returns a function that removes this registration and never a later one; keep it for the add-on’s Stop.
.GetRecentLogs
Section titled “.GetRecentLogs”Scribe.GetRecentLogs(filter: { Level: LogLevel?, Category: LogCategory?, Code: LogCode?, Limit: number? }?) → { LogEntry }Types: LogLevel, LogCategory, LogCode, LogEntry
Returns entries from the ring buffer in chronological order, so the newest is
the LAST element. The ring holds 512 entries unless the LogRingSize option
raises it. Limit keeps the most recent N. Optionally
filtered by level/category/code. Code, Level, and Category are typed
string unions, so editors autocomplete valid values (see the Log Code
Reference for the full list of codes).
.GetMetrics
Section titled “.GetMetrics”Scribe.GetMetrics() → { [string]: number | { Count: number, Average: number, Max: number } }A snapshot of internal counters (saves, loads, receipt outcomes, queue
depths, …) for admin panels and load tests. Plain counters are numbers;
timing and size distributions are { Count, Average, Max } records.
.GetPercentiles
Section titled “.GetPercentiles”Scribe.GetPercentiles() → { [string]: { P50: number, P90: number, P99: number, Samples: number, Age: number, Window: number } }Nearest-rank percentiles for every distribution GetMetrics
reports as a { Count, Average, Max } record: save durations, profile sizes,
per-frame flush costs, bytes per send.
The half GetMetrics cannot give you: an average hides the tail, and a P99 save
duration is what a player actually waits for on a bad key.
From a LocalScript it returns the client’s own distributions: the three apply
timers, elapsed seconds per frame received, inline listeners included.
Keyed by metric name. A name that has never been observed is absent rather
than present with zeroes, so next() on the result tells you whether anything
has been measured at all.
Computed over a rolling window of the most recent 256 samples per name, bounded
because an unbounded sample list on a metric observed once per save is a slow
leak. So these describe recent behaviour, and GetMetrics().<name>.Count is the
all-time count, which deliberately does not agree with the window. Samples is
how many the window holds, Age the seconds since the newest, and Window the
seconds the ring spans, so a caller can tell a burst from an afternoon.
.GetBudgetSnapshot
Section titled “.GetBudgetSnapshot”Scribe.GetBudgetSnapshot() → BudgetSnapshotTypes: BudgetSnapshot
The DataStore request allowance the engine currently reports, by request type.
local budget = Scribe.GetBudgetSnapshot()if budget.Available and budget.Budgets.UpdateAsync < 10 then warn("running low on save allowance")endAvailable is false when the engine could not be asked at all, such as on a
headless runner or in Studio with API access off. Treat that as no opinion: it
is not the same as an allowance of zero, and code that confuses the two stalls
forever in exactly the environments where there is nothing to conserve.
Purely a read. Global leaderboard background requests automatically use shared
budget pacing. BudgetPolicy additionally enables
faster backlog draining when allowance permits and budget waits during leaderboard
erasure. These settings do not change profile saves.
Leaderboards
Section titled “Leaderboards”.LeaderboardStatus
Section titled “.LeaderboardStatus”Scribe.LeaderboardStatus: { [string]: LeaderboardStatus }Types: LeaderboardStatus
The per-board states returned by Server.GetLeaderboardStatus.
Reasons
Section titled “Reasons”.LifecycleReason
Section titled “.LifecycleReason”Scribe.LifecycleReason: { [string]: LifecycleReason }Types: LifecycleReason
The same table as Scribe.Reason, under the name of the type it holds.
Scribe.Reason is the older name and both stay.