Code frameworks
Layouts and sheets
Draw a sheet with a Frame, bind it to your fields and wire its buttons to actions.
A Frame file
A Frame is a JSON file in layouts/. A Framework lists it in frames. This is the Keeper sheet.
"dataContract": {
"requiredBindings": [
{ "binding": "element.attributes.name", "expectedType": "string" },
{ "binding": "element.attributes.hp", "expectedType": "number" },
{ "binding": "element.attributes.max_hp", "expectedType": "number" }
],
"optionalBindings": [],
"actions": [
{ "name": "hurt", "functionRef": "hurt" },
{ "name": "heal", "functionRef": "heal" }
],
"regions": []
},- Lines 11-15 requiredBindings lists every field the text uses, with its type.
- Lines 17-20 Action aliases: a name for buttons to use, and the id of the action it calls.
layouts/keeper.json
{
"version": "1.0",
"metadata": {
"name": "Keeper sheet",
"bindingMode": "bound",
"frameworkRef": "keeper",
"context": "BLADE",
"template": "standard-blade"
},
"dataContract": {
"requiredBindings": [
{ "binding": "element.attributes.name", "expectedType": "string" },
{ "binding": "element.attributes.hp", "expectedType": "number" },
{ "binding": "element.attributes.max_hp", "expectedType": "number" }
],
"optionalBindings": [],
"actions": [
{ "name": "hurt", "functionRef": "hurt" },
{ "name": "heal", "functionRef": "heal" }
],
"regions": []
},
"layout": {
"kind": "container",
"children": [
{ "kind": "text", "text": "{{element.attributes.name}}" },
{
"kind": "text",
"text": "HP: {{element.attributes.hp}} / {{element.attributes.max_hp}}"
},
{ "kind": "button", "label": "Take damage", "actionRef": "hurt" },
{ "kind": "button", "label": "Heal", "actionRef": "heal" }
]
}
}| Part | What it holds |
|---|---|
| version | The Frame format version. Use 1.0, or 2.0 for a Frame that draws a list. |
| metadata | The name, bindingMode bound, and frameworkRef, the id of the Framework the Frame belongs to. Keep context and template as in the example. |
| dataContract | The fields the Frame reads and the actions its buttons may call. |
| layout | A tree of nodes. The example uses container, text and button nodes. |
A container takes its layout settings in a layout object next to kind. A text node takes style settings such as fontSize and fontWeight. A Frame holds no code, event handlers or raw HTML.
"kind": "container",
"layout": { "direction": "column", "gap": 12, "padding": 16 },
"children": [- Line 25 The node kind.
- Line 26 The layout settings: direction, gap and padding.
- Line 27 The nodes inside the container.
Bindings
Text shows a stored value with {{element.attributes.field}}. List every field you use in requiredBindings with its type. A binding to a field that does not exist is refused.
{
"kind": "text",
"text": "HP: {{element.attributes.hp}} / {{element.attributes.mana}}"
}FRAME_BINDING_UNKNOWN: element.attributes.manaButtons and actions
The dataContract lists action aliases. Each alias has a name and a functionRef, which is an action id of the same Framework. A button names an alias in actionRef.
"layout": {
"kind": "container",
"children": [
{ "kind": "text", "text": "{{element.attributes.name}}" },
{
"kind": "text",
"text": "HP: {{element.attributes.hp}} / {{element.attributes.max_hp}}"
},
{ "kind": "button", "label": "Take damage", "actionRef": "hurt" },
{ "kind": "button", "label": "Heal", "actionRef": "heal" }
]
}- Lines 26-30 Text nodes show fields with double braces.
- Lines 31-32 Buttons name an alias from dataContract.actions in actionRef.
{ "kind": "button", "label": "Take damage", "actionRef": "mend" }ACTION_MISSING: Action "mend" not found in framework's actionFunctions/ledgerFunctionsDice in text
Write a dice roll inside text, such as [[1d20+{{element.attributes.guard}}]]. The player rolls with the usual Fabled dice controls. A bound number must be a declared field and appear in the requiredBindings.
"text": "Defend: [[1d20+{{element.attributes.guard}}]]"- Line 39 A dice annotation sits in double square brackets. The field must be listed in requiredBindings.
How a roll reaches an action
A dice annotation shows a dice badge. When a player clicks it, Fabled rolls the dice and shows the result in the table's dice log and chat. The roll does not write to the ledger, and it does not fill an action input. An input with a roll key is rolled by the server instead, as described below.
A button cannot hold a value or a roll either. When a player uses the button of an action that has inputs, Fabled opens a dialog and asks for each input. So the steps are: roll, read the number, use the action's button, and type the number.
Write those steps on the sheet, so players know them. The rule must check the number, because the dialog does not limit it.
To have Fabled roll for the player, give the action input a roll key, as the Framework schemas page shows. The dialog then shows a Roll button in place of a field. The server rolls the dice and the dialog fills in the total in a field the player cannot edit. The player can roll again, and the new total replaces the old one.
A roll is spent once. When the action runs, it takes the roll the server kept, so a number cannot be typed or used twice. A new roll voids the earlier unspent rolls of that input. A roll is bound to the campaign it was made in, and it expires after 15 minutes. After that the player rolls again.
The roll is posted to the table log for an audience. If the action names no recipients, it goes to the player's roll audience, the same setting as their other dice rolls. Everyone is a public roll, only them is a whisper to themselves, and them and the GMs is a whisper to themselves and the GMs.
An action can post a result line to the table log with a log template. The Action log line section of the Framework schemas page shows how. A roll on the sheet posts only its own dice roll. Log lines are rolling out and may not be available in every game yet.
One Frame for each Framework
Every Framework has its own Frames. The Lamp child has its own card, bound to the Lamp fields.

